/*
**********************************************************************************************
******************************************公共方法*********************************************
**********************************************************************************************
*/
export const typeStr = val => Object.prototype.toString.call(val).slice(8, -1) // 获取类型
export const checkType = type => val => type === typeStr(val) // 科里化----->生成类型判断函数
export const isArray = checkType('Array') // 判断是否为数组类型
export const isObject = checkType('Object') // 判断是否是对象类型
export const hasObjKey = val => isObject(val) && !!Reflect.ownKeys(val).length // 是否是空对象
export const isReference = val => isArray(val) || isObject(val) // 判断是否是引用类型
export const isNull = checkType('Null') // 判断是否是null
export const isUndefined = checkType('Undefined') // 判断是否是undefined
export const isFunction = checkType('Function') // 判断是否是一个函数类型
export const isRegExp = checkType('RegExp') // 判断是否是正则表达式类型
export const isString = checkType('String') // 判断是否是字符串类型
export const isNumber = checkType('Number') // 判断是否是数字类型
export const isBoolean = checkType('Boolean') // 判断是否是布尔类型
export const isEmpty = val => (isArray(val) && val.length==0) || (isObject(val) && Object.keys(val).length == 0) || isNull(val) || isUndefined(val) || val === ''
export const isMap = checkType('Map') // 判断是否是Map类型
export const isSet = checkType('Set') // 判断是否是Set类型
export const isSymbol = checkType('Symbol') // 判断是否是Symbol类型
export const isDate = checkType('Date') // 判断是否是Date类型
export const isError = checkType('Error') // 判断是否是Error类型
export const isBlob = checkType('Blob') // 判断是否是Blob类型
export const isGt0 = val => /^\+?[1-9][0-9]*$/.test(val) // 是否是大于0的整数
export const isGtEq0 = val => /^\+?[1-9][0-9]*$/.test(val) || String(val) === '0' // 是否是大于等于0的整数
export const inBrowser = typeof window !== 'undefined' // 是否是在浏览器环境
export const inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform // 是否在微信环境中
export const weexPlatform = inWeex && WXEnvironment.platform.toLowerCase()
export const UA = inBrowser && window.navigator.userAgent.toLowerCase()
export const isIE = UA && /msie|trident/.test(UA) // 是否在IE环境
export const isIE9 = UA && UA.indexOf('msie 9.0') > 0 // 是否是IE9
export const isEdge = UA && UA.indexOf('edge/') > 0 // 是否是Edge浏览器
export const isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android') // 是否是安卓环境
export const isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios') // 是否是ios环境
export const isWeChat = (UA && (/MicroMessenger/i).test(UA)) // 是否是微信环境
export const isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge // 是否是谷歌浏览器
export const isPhantomJS = UA && /phantomjs/.test(UA)
export const isFF = UA && UA.match(/firefox\/(\d+)/) // 是否是火狐浏览器
export const isPhone = val => /^1[3456789]\d{9}$/.test(val) // 检测是否是手机号码
export const isIdentity = val => /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/.test(val) // 身份证 321281155121152489
export const isEmail = val => /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(val) // 判断是否是正确的邮箱
export const isInVsCode = () => !!window.acquireVsCodeApi
export const isDom = val => val instanceof Element // 判断是否是dom元素
export const isHTML = val => /<\/?[a-z][\s\S]*>/i.test(val)
export const isIpv4 = ip => /^(25[0-5]|2[0-4]\d|1\d{2}|\d{1,2})\.(25[0-5]|2[0-4]\d|1\d{2}|\d{1,2})\.(25[0-5]|2[0-4]\d|1\d{2}|\d{1,2})\.(25[0-5]|2[0-4]\d|1\d{2}|\d{1,2})$/.test(ip)
export const isValidId = id => id !== null && id !== undefined && id !== '' && !isBoolean(id) // 判断是否是正确的id
export function isMobile() {  
  const userAgent = navigator.userAgent || navigator.vendor || window.opera;  
  return /android|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent.toLowerCase());  
}
// 获取设备类型名称
export function getDeviceName() {
  const ua = navigator.userAgent.toLowerCase()
  const width = window.innerWidth
  const isAndroidTablet = /android/i.test(ua) && !/mobile/i.test(ua) // 安卓平板特征
  const isExplicitTablet = /ipad|tablet|playbook|silk/i.test(ua) || isAndroidTablet
  // 增加屏幕方向检测（优化折叠屏展开态）
  const isImplicitTablet = (width >= 768 && width < 1024) && ('ontouchstart' in window || navigator.maxTouchPoints > 0)
  if (isExplicitTablet || isImplicitTablet) return 'pad'
  // 优化折叠屏UA特征（如Galaxy Z系列）
  const isFoldable = /android.*z fold/i.test(ua)
  const isMobile = /mobile|iphone|ipod|iemobile|opera mini|(android.*mobile)/i.test(ua)
  return (isMobile || width < 768 || isFoldable) ? 'mobile' : 'pc'
}
/**
 * 创建一个哈希表，返回一个函数。（用于检查传入的值是否是哈希表中的值）
 * @param {String} str 字符串
 * @param {Boolean} expectsLowerCase 是否区分大小写   默认为false即不区分    传true为区分
 * @returns 一个判断函数
 * @举例 const isTag = makeMap('div,span', true); isTag('div')
 */
export function makeMap(str, expectsLowerCase = false) {
  assert(isString(str), '您传入的参数str有误，必须是一个字符串')
  let [map, list] = [Object.create(null), str.split(',')]
  for(var i = 0, len = list.length; i < len; i++) { map[list[i]] = true }
  return expectsLowerCase ? val => map[val.toLowerCase()] : val => map[val]
}
/*
**********************************************************************************************
******************************************字符串操作*********************************************
**********************************************************************************************
*/
/**
 * 数据转为字符串
 * @param {*} val 需要转成字符串的值
 * @returns 
 * @举例 toString([{name:'zzz', age:18}]) ---> '[\n  {\n    "name": "asd",\n    "ae": "as"\n  }\n]'
 */
export const toString = val => val == null ? '' : isReference(val) ? JSON.stringify(val, null, 2) : String(val)
/** 返回是否以某个字符串开头
 * @param {String} str 目标字符串
 * @param {String} keywords 需要搜索的开头的字符串
 * @returns {Boolean} 返回布尔值，是否验证通过
 * @举例 startWith('asdasd', 'as') ---> true
 */
export const startWith = (str, startWords) => str.slice(0, startWords.length) === startWords
/**
 * 去除字符串中的空格
 * @param {String} str 需要去除空格的字符串
 * @param {Number} type 去除空格的类型 ----> 'headTail': 去除首尾空格   'all'：去除全部空格  'head'：去除头部空格  'tail'：去除尾部空格
 * @returns {String} 返回处理后的字符串
 * @举例 trim(' ab c  ')  ---> 'ab c' // 'headTail':去除首尾空格
 * @举例 trim(' ab c  ', 'all')  ---> 'abc' // 'all':去除全部空格
 * @举例 trim(' ab c  ', 'head')  ---> 'ab c  ' // 'head'：去除头部空格
 * @举例 trim(' ab c  ', 'tail')  ---> ' ab c' // 'tail'：去除尾部空格
 */
export function trim(str = '', type = 'headTail') {
  const mapReg = { 'headTail': new RegExp(/(^\s*)|(\s*$)/g),'all': new RegExp(/\s+/g),'head': new RegExp(/(^\s*)/g),'tail': new RegExp(/(\s*$)/g) }
  return str.replace(mapReg[type] || '', '')
}
/**
 * 固定裁剪几个字符之后显示省略号
 * @param {String} str 需要进行裁剪的字符串
 * @param {Number} num 要裁剪几位数字
 * @returns {String} 返回处理后的字符串
 * @举例 sliceStr('张三李四王五', 2) ----> "张三..."
 */
export const sliceStr = (str, num) => String(str).length > num ? String(str).slice(0, num) + '...' : String(str).slice(0, num)
/**
 * splitStr分割字符串
 * @param {*} str 字符串
 * @param {*} index 分割位置
 * @returns 
 */
export const splitStr = (str, index) => [str.slice(0, index), str.slice(index)]
/**
 * 字符串前置补0
 * @param {String} str 
 * @param {Number} num 
 * @returns 
 * @举例 addZero('1', 2) ----> '01'
 */
export const addZero = (str = '', num = 2) => (Array(num+1).join('0') + String(str)).slice(-num)
// 完美的统计字符串长度，能正确统计占四个字节的Unicode字符。举例：length('x\uD83D\uDE80y') ----> 3
export const length = str => [...str].length
// 字符串字节数
export const charLen = value => [...(value || '')].reduce((prev, item) => prev + (item.charCodeAt() > 255 ? 2 : 1), 0)
/**
 * 字符串复制
 * @param {String} e 需要复制的文本
 * @举例 copyLink('10086')  ----> ctrl + v 就能粘贴出10086
 */
export function copyLink(e){
  // if(!e) { return this.$Message.error('链接地址为空') }
  var input = document.createElement("input") // js创建一个input输入框
  input.value = e // 将需要复制的文本赋值到创建的input输入框中
  document.body.appendChild(input) // 将输入框暂时创建到实例里面
  input.select() // 选中输入框中的内容
  document.execCommand("Copy") // 执行复制操作
  document.body.removeChild(input) // 最后删除实例中临时创建的input输入框，完成复制操作
  // return this.$Message.success('复制成功')
}
/**
 * 字符串首字母大写
 * @param {String} str 
 * @returns 经过转换后的首字母为大写的字符串
 * @举例 capitalize('abcd')  ---->  'Abcd'
 */
export const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1)
/*
**********************************************************************************************
******************************************数组方法*********************************************
**********************************************************************************************
*/
/**
 * 洗牌算法
 * @param {Array} arr 原始数组
 * @returns {Array} 打乱顺序后的数组
 * @举例 shuffle([1,2,3,4])  ---> 可能的结果：[2,4,1,3]
 */
export function shuffle(arr){
  if(!isArray(arr)) { arr = [arr] }
  for(let i = arr.length - 1; i > 0; i--) {
    const randomIndex = Math.floor(Math.random() * (i + 1))
    ;[arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]]
  }
  return arr
}
/**
 * 缓存函数计算结果
 * @param {Function} fn 需要做缓存处理的函数
 * @returns {Function} 新的有缓存效果的函数
 * @举例 const cachedComputed = cached(function(val){ return val + 'ZZZ' })
 * @测试 cachedComputed('abc') ---> 'absZZZ' 第二次调用就不需要计算了直接取值 cachedComputed('abc') ---> 'absZZZ'
 */
export function cached(fn) {
  const cache = Object.create(null)
  return str => (!cache[str] && (cache[str] = fn(str)), cache[str])
}
/**
 * 扩展对象
 * @param {Object} to 需要扩展的目标对象
 * @param {Object} _from 从这个对象扩展
 * @returns 扩展之后的对象
 * @举例 extend({}, {name:1}) ----> {name: 1}
 */
export function extend(to, _from) {
  for(var key in _from) { to[key] = _from[key] }
  return to
}
/**
 * 对象数组转对象
 * @param {Array} arr 需要转换的数组
 * @returns {Object} 转换之后的对象
 * @举例 toObject([{name: 1}, {age:2}]) ----> { name:1, age:2 }
 */
export function toObject(arr) {
  var res = {}
  for(var i = 0; i < arr.length; i++) { arr[i] && extend(res, arr[i]) }
  return res
}
/**
 * 改变函数执行的this指向
 * @param {function} fn 需要转换的数组
 * @returns {context} ctx 执行环境
 * @举例 polyfillBind(fn, this) ----> (...args) => this.fn(...args)
 */
export function polyfillBind(fn, ctx) {
  function boundFn(a) {
    var l = arguments.length
    return l
      ? l > 1
        ? fn.apply(ctx, arguments)
        : fn.call(ctx, a)
      : fn.call(ctx)
  }
  boundFn._length = fn.length
  return boundFn
}
/**
 * 是否匹配当前条目
 * @param {*} item 当前条目
 * @param {*} search 搜素条件，可以是对象也可以是函数
 * @param {*} index 当前判断的下标，用于数组遍历的时候判断，传入index，可以不传
 * @returns 
 * @举例子 isCurItem({name: 'zaz', age: 18}, v => v.name == 'zaz') -----> true
 * @举例子 isCurItem({name: 'zaz', age: 18}, v => v.name == 'ss') -----> false
 * @举例子 isCurItem({name: 'zaz', age: 18}, { name: 'zaz' }) -----> true
 * @举例子 isCurItem({name: 'zaz', age: 18}, { name: 'ss' }) -----> false
 */
export function isCurItem(item, search, index) {
  return isObject(search) ? Object.keys(search).reduce((prev, v) => (prev = prev && search[v] == item[v], prev), true) : search(item, index)
}
/**
 * 断言函数，用于做函数类型判断
 * @param {Boolean} condition 
 * @param {String} message 
 * @举例 assert(isArray(arr) && arr.every(v => isObject(v)), 'arr必须为对象数组')
 */
export function assert (condition, message) { 
  if (!condition) { throw new Error(message) }
}
/**
 * 匹配目标对象进行字段调整，支持递归修改
 * @param {Array<Object>} arr 需要操作的数据
 * @param {Object|Function} search 需要查询的json对象 | 也可以传入校验函数
 * @param {Function} success 成功匹配的处理函数--->接受一个item，返回一个新的item
 * @param {Function} fail 失败匹配的处理函数---->接受一个item，返回一个新的item
 * @param {String} field 需要递归处理的字段，默认递归处理children字段
 * @举例 
  searchCover(
    [
      {id:1, type: 'aa', age:17},
      {id:2, type: 'bb', age:20},
      {id:3, type: 'cc', age:21}
    ],
    v => v.age < 18, // 代表匹配age <= 20的数据
    v => ({ ...v, type: '未成年' }),  // 匹配成功执行这个函数
    v => ({ ...v, type: '成年' })  // 匹配失败的执行这个函数
  )
  得到如下结果-----------> 
  [
    {id: 1, type: '未成年', age: 17},
    {id: 2, type: '成年', age: 20},
    {id: 3, type: '成年', age: 21}
  ]
 * @举例 支持递归改值
  searchCover(
    [
      {
        id:1,
        isChecked: false,
        children: [
          {
            id:11,
            isChecked: true
          }
        ]
      },
      {
        id:1,
        isChecked: false,
      }
    ],
    v => v.id == 1,
    v => ({...v, isChecked: true }),
    v => ({...v, isChecked: false }),
  )
  得到如下结果-------------->
  [
    {
      "id": 1,
      "isChecked": true,
      "children": [
        {
          "id": 11,
          "isChecked": false
        }
      ]
    },
    {
      "id": 1,
      "isChecked": true
    }
  ]
 */
export function searchCover(arr, search = v => v, success = v => v, fail = v => v, field = 'children') {
  if(isArray(arr)) { return arr.map(item => searchCover(item, search, success, fail, field)) }
  const newItem = isCurItem(arr, search) ? success(arr) : fail(arr)
  return Object.entries(newItem).reduce((prev, item) => {
    const [key, val] = item
    return { ...prev, [key]: key == field && isArray(val) ? searchCover(val, search, success, fail, field) : val }
  },{})
}
/**
 * 单选
 * @param {Array} arr 
 * @param {Function} search 
 * @returns 
 * @举例 radioChecked([{name:'zz', id:1, isChecked:false}, {name:'zs', id:2, isChecked:true}], {id: 1})
 * ----> [{name:'zz', id:1, isChecked:true}, {name:'zs', id:2, isChecked:false}]
 */
export const radioChecked = (arr, search = v => v, field = 'isChecked') => searchCover(arr, search, v => ({...v, [field]:true}), v => ({...v, [field]:false}))
/**
 * 单选允许取消勾选
 * @param {Array} arr 
 * @param {Function} search 
 * @returns 
 * @举例 radioToogle([{name:'zz', id:1, isChecked:false}, {name:'zs', id:2, isChecked:true}], {id: 1})
 * ----> [{name:'zz', id:1, isChecked:true}, {name:'zs', id:2, isChecked:false}]
 * @举例 radioToogle([{name:'zz', id:1, isChecked:true}, {name:'zs', id:2, isChecked:false}], {id: 1})
 * ----> [{name:'zz', id:1, isChecked:false}, {name:'zs', id:2, isChecked:false}]
 */
export const radioToogle = (arr, search = v => v, field = 'isChecked') => searchCover(arr, search, v => ({...v, [field]:!v[field]}), v => ({...v, [field]:false}))
/**
 * 多选逻辑
 * @param {Array} arr 
 * @param {Function} search 
 * @returns 
 * @举例 
 * multipleChecked([{name:'zz', id:1, isChecked:false}, {name:'zs', id:2, isChecked:true}], {id: 1})
 * ----->
 * [{name:'zz', id:1, isChecked:true}, {name:'zs', id:2, isChecked:true}]
 */
export const multipleChecked = (arr, search = v => v, field = 'isChecked') => searchCover(arr, search, v => ({...v, [field]:!v[field]}))
/**
 * 对象数组去重(根据对象中某个字段)
 * @param {Object<Array>} arr 需要去重的对象数组
 * @param {*} field 字段名称
 * @param {String} type  'first': 有重复的对象则取遍历到的第一个 | 'last'有重复的则取遍历到的最后一个
 * @param {String} equal  是否严格校验 strict: 严格校验===   loose：宽松校验，忽略大小写==
 * @returns 去重后的对象数组
 * @举例 根据对象中id字段进行去重操作
 * @举例 uniqueObj([{id:1, age:11}, {id:2, age:22}, {id:1, age: 33}], 'id')  ---->  [{"id": 1,"age": 11,"_sort": 0},{"id": 2,"age": 22,"_sort": 1}]
 * @举例 uniqueObj([{id:1, age:11}, {id:2, age:22}, {id:1, age: 33}], 'id', 'last') ----> [{"id": 1,"age": 33,"_sort": 0},{"id": 2,"age": 22,"_sort": 1}]
 * @举例 uniqueObj([{id:1, age:11}, {id:2, age:22}, {id:1, age: 33}, {id:'1', age:44}], 'id', 'first', 'strict') ---->  [{"id": 1,"age": 11,"_sort": 0},{"id": 2,"age": 22,"_sort": 1},{"id": "1","age": 44,"_sort": 3}]
 */
export function uniqueObj(arr, field = need('field'), type = 'last', equal = 'loose') {
  assert(isArray(arr) && arr.every(v => isObject(v)), 'arr必须为对象数组')
  const obj = arr.reduce((prev, item, index) => {
    let key = item[field]
    if(equal == 'strict' && isNumber(key)) { key = `_${key}`}
    const [existItem, curItem] = [prev[key], { ...item, _sort: index }]
    prev[key] = type == 'first' ? (existItem || curItem) : existItem ? { ...item, _sort: existItem._sort } : curItem
    return prev
  }, {})
  return Object.values(obj).sort((a, b) => a._sort - b._sort)
}
/**
 * 扁平数组转对象tree树形结构
 * https://juejin.cn/post/6983904373508145189#heading-8
 * @param {Array} arr 需要转换的数组
 * @param {String} field 子元素数组的字段值
 * @returns {Array} 转换之后的数组
 * @注意 pid为0为一级目录
 * @举例 
 * let arr = [
 *   {id: 1, name: '部门1', pid: 0},{id: 2, name: '部门2', pid: 1},{id: 3, name: '部门3', pid: 1},
 *   {id: 4, name: '部门4', pid: 3},{id: 5, name: '部门5', pid: 4},{id: 9, name: '部门6', pid: 0},
 * ]
 * flat2tree(arr)
 * -----> 
 * [
 *   {
 *     "id": 1,
 *     "name": "部门1",
 *     "pid": 0,
 *     "children": [ { "id": 2, "name": "部门2", "pid": 1, "children": [] }, { "id": 3, "name": "部门3", "pid": 1, "children": [{...},{...}] } ]
 *   }
 *   {
 *     "id": 6,
 *     "name": "部门6",
 *     "pid": 0,
 *     "children": []
 *    }
 * ]
 */
export function flat2tree(arr, field = 'children', key = 'id', pidKey = 'pid') {
  const itemMap = new Map()
  const tree = []
  for (const item of arr) {
    const { [key]: id, [pidKey]: pid } = item
    if (!itemMap.has(id)) itemMap.set(id, { ...item, [field]: [] }) // 初始化节点
    const curItem = itemMap.get(id)
    if (pid === 0) {
      tree.push(curItem) // 根节点
    } else {
      !itemMap.has(pid) && itemMap.set(pid, { [field]: [] })// 确保父节点存在
      itemMap.get(pid)[field].push(curItem) // 添加到父节点
    }
  }
  return tree
}
/**
 * 对象数组递归扁平化，算法性能O(n)
 * @param {Array} arr 需要平铺的数组
 * @param {String} field 递归子字段名称（默认 'children'）
 * @param {String} key 唯一标识字段（默认 'id'）
 * @param {Number|String} pid 父级 ID（默认 0）
 * @returns {Array} 扁平化后的数组
 * @举例
 * const arr = [
 *  { id:1, name: 'a', children: [ { id:11,name: 'b', children: [{ id:111,name: 'c', children: [] }]}] },
 *  { id:2, name: 'd', children: [ { id:22,name: 'e', children: [{ id:222,name: 'f', children: [] }]}] }
 * ]
 * tree2flat(arr, 'children')
 * ---->
  [
    {"id": 1,"name": "a","children": [],"pid": 0},{"id": 11,"name": "b","children": [],"pid": 1},{"id": 111,"name": "c","children": [],"pid": 11},
    {"id": 2,"name": "d","children": [],"pid": 0},{"id": 22,"name": "e","children": [],"pid": 2},{"id": 222,"name": "f","children": [],"pid": 22}
  ]
// 拓展：O(n2)写法，但是更简洁
// export function tree2flat(arr, field = 'children', key = 'id', pid = 0) {
//   return arr.reduce((prev, item) => [...prev, {...item, pid, [field]: []}, ...tree2flat(item[field] || [], field, key, item[key])], [])
// }
 */
export function tree2flat(arr, field = 'children', key = 'id', pid = 0) {
  const result = []
  function flatten(nodes, parentId) {
    for (const item of nodes) {
      const {[field]: myField, [key]:myKey, ...rest} = item
      result.push({...rest, pid: parentId, [key]:myKey }) // result.push({...rest, pid: parentId, [key]:myKey,[field]:[] })
      Array.isArray(myField) && flatten(myField, myKey) // 递归处理子节点
    }
  }
  flatten(arr, pid)
  return result
}
/**
 * 一次性函数。每次第一次加载的时候只执行一次。后面再调用,没有任何函数代码执行
 * 示例：const aa = _once(function (a, b){console.log(a + b)})
 * aa(1,2) ===> 3   ------>  aa(3, 4) ===> undefined
 */
export function _once(fn) {
  var called = false
  return function () {
    if(!called) { called = true;fn.apply(this, arguments) }
  }
}
/**
 * 一次性函数。软件生命周期，只执行一次（只有刷新才会重置）。后面再调用,没有任何函数代码执行
 * 示例：const aa = once(function (a, b){console.log(a + b)})
 * aa(1,2) ===> 3   ------>  aa(3, 4) ===> undefined
 */
export function once(key = 'key', fn) {
  if(!window[key]) {
    window[key] = true;fn.apply(this, arguments)
  }
}
/**
 * 删除数组中某一个元素
 * @param {Array} arr 需要操作的数组
 * @param {*} item 要删除的条目
 * @param {String} type 删除类型  'first': 代表只删除查到的第一个, 'all'：代表删除查到的全部， 'last'：代表删除查到的最后一个
 * @returns 删除后的新数组
 * @举例 remove([1,2,3,4,5,4], 4, 'all') ----> [1,2,3,5]
 * @举例 remove([1,2,3,4,5,4], 4, 'first') ----> [1,2,3,5,4]
 * @举例 remove([1,2,3,4,5,4], 4, 'last') ----> [1,2,3,4,5]
 */
export function remove(arr, item, type = 'all') {
  const mapType = {
    'last': () => arr.splice(arr.lastIndexOf(item), 1),
    'first': () => arr.splice(arr.indexOf(item), 1),
    'all': () => arr = arr.filter(v => v !== item),
  }
  return (mapType[type] && mapType[type]()) || arr
}
/**
 * 数组、字符串元素复制N次 
 * @param {Object|Array} obj 
 * @param {Number} times 
 * @returns 复制之后的数据
 * @举例 重复生成数组元素：repeat([{age:1}], 2) ====>[{age:1, _id: 'asdasd2j2'}, {age:1, _id: '123123c'}]  // 备注增加_id是为了for循环的key值不重复
 * @举例 重复生成字符串：repeat('abc', 2) ====>  'abcabc'
 * @举例 字符串复制实现：Array(3).join(0) ====> '00'    "0".repeat(2) ===> '00'
 * @举例 引用类型复制实现：Array(2).fill([{name: '张三'}]) ====> [[{name: '张三'}], [{name: '张三'}]]
 */
export function repeat(obj = '', times = 1) {
  const count = range(times, 1)
  let res = isArray(obj) ? [] : ''
  if(isArray(obj)) {
    for(let i=0; i<count; i++) { res = isObject(obj[0]) ? [...res, ...deepCopy(obj).map(v => ({ ...v, _id: guID() }))] : [...res, ...obj] }
  } else {
    for(let j=0; j<count; j++) { res += obj }
  }
  return res
}
/**
 * 获取特定条件的对象数组中，满足条件的对象的字段值，返回数组形式
 * @param {Array} arr 需要处理的数组
 * @param {String} field 需要获得的字段
 * @param {Function|Object} search 过滤函数|过滤对象，如果不传则返回全部数组中的字段值     如果传了函数，则先按照函数过滤一遍 
 * @returns 得到的字段组装的数组
 * @举例 getFieldArr([{id:1, age: 15}, {id: 2, age: 18}, {id:3, age: 20}], 'id', v => v.age > 16) --->  [2, 3]
 * @举例 getFieldArr([{id:1, age: 15}, {id: 2, age: 18}, {id:3, age: 20}], 'id', {age:18}) --->  [2]
 */
export function getFieldArr(arr, field, search = v => v) {
  return arr.reduce((prev, item) => isCurItem(item, search) ? [...prev, item[field]] : prev, [])
}
/**
 * 对数组进行排序。根据数组中对象的字段。按照传入数组的顺序进行排序
 * @param {Array<Object>} arr 当前需要排序的数组
 * @param {String} key 排序字段
 * @param {Array<String>} sortArr 排序数组
 * @param {String} type 类型 'keep': 保留不在sortArr的数据    'drop':舍弃不在sortArr中的字段
 * @举例子
 * sortByCusField([{"key": "name"}, {"key": "age"}, {"key": "xiaoming"}], 'key', ['age', 'name', 'xiaoming'])
 * ----->
 * [{"key": "age"}, {"key": "name"}, {"key": "xiaoming"}]
 */
export function sortByCusField(arr, key, sortArr, type = 'keep') {
  const mapSort = sortArr.reduce((prev, item, index) =>(prev[item] = index + 1, prev), {})
  const [inSortArr, outSortArr] = filterArr(arr, v => mapSort[v[key]] > 0)
  const newSortArr = inSortArr.sort(({[key]: left}, {[key]: right}) => mapSort[left] - mapSort[right])
  return type == 'keep' ? [...newSortArr, ...outSortArr] : newSortArr
}
/**
 * 对象数组按照某个字段进行排序
 * @param {Array<Object>} arr 需要排序的对象数组
 * @param {String} prop 根据这个字段的值进行排序
 * @param {Number | String} type 排序方式、递增还是递减 'increase': 递增   'decrease'递减
 * @returns {Array} 排序后的数组
 * @举例 sortByKey([{name:'ss', age:30}, {name:'dd', age:14}], 'age') ----> [{name:'dd', age:14}, {name:'ss', age:30}]
 */
export function sortByKey(arr, key, type = 'increase') {
  return arr.sort(({[key]: left},  {[key]: right}) => type == 'increase' ? left - right : right - left)
}
/**
 * 按照传入的删选条件过滤数组，符合条件的在左边，不符合的在右边
 * @param {Array} arr 需要处理的数据
 * @param {Function} fn 筛选函数
 * @returns {[[], []]} 返回数组两个元素分别为符合条件和不符合条件的
 * @举例 filterArr([23, 5, 7, 29, 9], v => v <= 10) ----> [[5, 7, 9],[23, 29]]
 * @举例 filterArr([23, 5, 7, 29, 9], (v, i) => i < 3) ----> [[23, 5, 7],[29, 9]]
 */
export function filterArr(arr, fn) {
  return arr.reduce((prev, item, index) => (isCurItem(item, fn, index) ? prev[0].push(item) : prev[1].push(item), prev), [[], []])
}
/**
 * 按照条件分割大数组为多个小数组的集合
 * @param {*} arr 被分割的数组
 * @param {*} fn 分割函数，可以传入对象
 * @returns 
 * @举例子 splitArr([1,2,3,4,5,3,7], v => v == 2) ------->  [[1],[3,4,5,3,7]]
 * @举例子 splitArr([1,2,3,4,5,3,7], v => v == 3) ------->  [[1,2],[4,5],[7]]
 * @举例子 splitArr([1,2,3,4,5,3,7], v => v == 1) ------->  [[2,3,4,5,3,7]]
 * @举例子 splitArr([1,2,3,4,5,3,7], v => v == 99) ------->  [[1, 2, 3, 4, 5, 3, 7]]
 */
export function splitArr(arr, fn) {
  return arr.reduce((prev, item, index) => (isCurItem(item, fn, index) ? prev.push([]) : prev.at(-1).push(item), prev), [[]])
}
/**
 * 数组分块
 * @param {Array} arr 需要分块的数组
 * @param {Number} size 每个数组块几个与元素
 * @returns 分块之后的数组
 * @举例 chunk([1,2,3,4,5], 2) ====>   [[1,2], [3, 4], [5]]
 */
export function chunk(arr, size = 0) {
  assert(isArray(arr), 'arr必须是数组类型')
  size = Number(size)
  assert(isGt0(size), 'size必须为大于0的整数')
  var targetArr = []
  for(var i = 0; i < arr.length; i += size) { targetArr.push(arr.slice(i, i + size)) }
  return targetArr
}
/**
 * 将数组中的数据进行分类，分类成JSON。键名为类别名称，键值为数组，存放数据集合
 * @param {Array} arr 需要分类的数组
 * @param {Function|String} fn_field 分类函数 | 字段名
 * @举例1 简单根据某个字段分类
 * const arr = [{name: '小明', age: 17}, {name: '小张', age: 17}, {name: '小强', age: 22}]
 * groupBy(arr, 'age')   |   groupBy(arr, item => item.age)
 * -----> 根据年龄分类好的结果
 * {17: [{name: '小明', age: 17},{name: '小张', age: 17}], 22: [{name: '小强', age: 22}]}
 * @举例2 根据某个字段进行处理，用处理后的结果分类
 * const arr = [{name: 'asd', score: 100}, {name: '3dd', score: 60}, {name: 'dfg', score: 80}, {name: 'zrr', score: 90}]
 * groupBy(arr, item => {
 *   const { score } = item
 *   return score < 65 ? 'E' :
 *          score < 70 ? 'D' :
 *          score < 80 ? 'C' :  
 *          score < 90 ? 'B' : 'A';
 *   })
 * -------> 根据分类函数分类好的结果：
 * {A: [{...},{...}], B: [{...}], C: [{...}], D: [{...}]}
 */
export function groupBy(arr, fn_field){
  return arr.reduce((prev, item) => {
    const key = isFunction(fn_field) ? fn_field(item) : item[fn_field]
    return (prev[key] || (prev[key] = [])).push({...item, _classifyKey: key}), prev
  }, {})
}
/**
 * 取两个数组元素的交集
 * @param {*} arr1 数组1
 * @param {*} arr2 数组2
 * @returns 取交集之后的结果
 * @举例 intersect([1,2,3], [1,2]) ----> [1, 2]
 * @举例 intersect('1,2,3', '1,2') ----> [1, 2]
 */
export function intersect(v1, v2){
  ;[v1, v2] = [str2Arr(v1), str2Arr(v2)]
  return v1.filter(x => new Set(v2).has(x))
}
/**
 * 数组（a 相对于 b 的）差集:  a数组中的数据，在b数组中没找到的数据
 * @param {Array|String} v1 数据1
 * @param {Array|String} v2 数据2
 * @param {Boolean} strictEqual 类型  strictEqual为true则为严格匹配   默认是不区分数字和字符串这样的
 * @param {String} isSet 是否是集合，如果是集合的话，是不允许出现重复元素的
 * @returns 
 * @举例子 difference([1,2,3], [1,2,7]) ====> [3]
 * @举例子 difference('1,2,3', '2') ====> '1,3'
 * @举例子 difference('1,2,3', '2,8') ====> '1,3'
 */
export function difference(v1, v2, strictEqual = false, isSet = false){
  const transArr = v => strictEqual ? str2Arr(v) : str2Arr(v).map(v => String(v))
  ;[v1, v2] = [transArr(v1), transArr(v2)]
  if(isSet) {
    return v1.filter(x => !new Set(v2).has(x))
  } else {
    let [res, map] = [[], new Map()]
    v1.forEach(v => map.set(v, (map.get(v) || 0) + 1))
    v2.forEach(v => map.set(v, (map.get(v) || 0) - 1))
    for (let [k,v] of map.entries()) {
      if(v > 0) { res.push(...new Array(Number(v)).fill(k)) }
    }
    return res
  }
}
/**
 * 数据1---是否包含---数据2--的数据。type：1部分包含，type：2全部包含
 * @param {Array|String} v1 
 * @param {Array|String} v2 
 * @param {Number} type  'some':则为部分包含  |  'every':全部包含
 * @param {Boolean} equal 是否严格校验 strict: 严格校验===   loose：宽松校验，忽略大小写==
 * @param {String} split 分隔符 默认为逗号
 * @return {Boolean} 返回校验结果
 * @举例子 isContain('1,2,3', '2,3') ====> true
 * @举例子 isContain('1,2,3', '2,3,4') ====> true
 * @举例子 isContain('1,2,3', '2,3,4', ) ====> false
 */
export function isContain(v1, v2, type = 'some', equal = 'loose', split = ',') {
  const lowerCase = v => v.map(k => String(k).toLowerCase())
  if(isString(v1)) { v1 = v1.split(split) }
  if(isString(v2)) { v2 = v2.split(split) }
  if(equal == 'loose') { [v1, v2] = [lowerCase(v1), lowerCase(v2)] }
  return type == 'some' ? v2.some(v => v1.some(k => equal ? k == v : k === v)) : v2.every(v => v1.some(k => strictEqual ? k == v : k === v))
}
/**
 * 数组（a 和 b 的）并集
 * @param {*} arr1 数组1
 * @param {*} arr2 数组2
 * @returns 
 * @举例子 union([1,2,3], [1,2,7]) ====> [1,2,3,7]
 */
export const union = (arr1, arr2) => [...new Set([...arr1, ...arr2])]
/*
**********************************************************************************************
******************************************JSON操作*********************************************
**********************************************************************************************
*/
/**
 * 格式化JSON, 将null, undefined,转换为''，否则后端会认为undefined和null为字符串导致bug
 * @param {Object} obj
 * @returns 
 * @举例子 formatJSON({name:null, age:undefined, school: '清华大学'}) ---> {name:'', age:'', school: '清华大学'}
 */
export function formatJSON(obj) {
  if(!isReference(obj)) { return obj }
  return isObject(obj) ? Object.keys(obj).reduce((prev, item) => ((prev[item] = isNull(obj[item]) || isUndefined(obj[item]) || obj[item] == 'undefined' || obj[item] == 'null'  ? '' : obj[item]), prev), {}) : {}
}
/**
 * 检查表单必填项是否为空，空则返回第一个为空的字段名。用于做一些必填校验检查
 * @param {Object} obj 
 * @returns 
 * @举例 checkParams({name:'张三', age:'', school:''}) ----> 'age'
 */
export const checkJSON = obj => Object.keys(obj).find(item => !Boolean(obj[item])) || ''
/**
 * JSON转url（这个函数将数据进行了编码。将来再解码使用。可以规避一些特殊字符产生的bug）
 * 函数还兼容传入 {info: {name:'zz', age:18}, school: 'qinghua'} 这种复杂的数据。之后通过url2JSON可以完美解析
 * @param {String} url 跳转地址的域名。在小程序中那就是路径
 * @param {Object} params 跳转地址中药传递的参数的json格式
 * @param {String} type  'nocode'不需要编码  |  'encode': 需要编码(默认值)
 * @returns {String} 返回拼接好的带有参数的链接地址
 * @举例子 JSON2url('../advise/index', { from: 'index', id_str:'1243' }) -----> '../advise/index?from=index&id_str=1243'
 */
export function JSON2url(url = '', params = {}, type = 'nocode'){
  return Object.entries(formatJSON(params)).reduce((prev, item) => {
    let [key, val] = item
    val = type === 'nocode' ? val : encodeURIComponent(val) // 为了适配更多的场景，开发了自定义是否编码
    const symbol = url ? (prev.includes('?') ? '&' : '?') : (prev ? '&' : '')
    return prev + symbol + `${key}=${val}`
  }, url) || ''
}
/**
 * url转JSON(函数内与解码操作，与JSON2url相对应)
 * @param {String} url 传入带有参数的url链接地址
 * @param {String} type 'nocode'不需要编码  |  'encode': 需要编码(默认值)
 * @returns {Object} 返回参数拼接的json对象
 * @举例 url2JSON('http://www.baidu.com?name=asd&age=12') ----> {name: "asd", age: "12"}
 * @举例 url2JSON('name=asd&age=12') ----> {name: "asd", age: "12"}
 */
export function url2JSON(url = '', type = 'nocode') {
  // url = url || window.location.href // 如果没传参，就使用浏览器当前url。暂时注释，因为这个不兼容小程序
  let paramsStr = url.includes('?') ? (url.split('?')[1] || '') : url
  paramsStr = paramsStr.split('#')[0] || '' // 防止一些url中混入#号放在?号之后，此处做一个适配
  return paramsStr.split('&').reduce((prev, item) => {
    let [key, val] = item.split('=')
    val = type == 'nocode' ? val : decodeURIComponent(val)  // 为了适配更多的场景，开发了自定义是否解码（如果传入的url是编码过的，那么必须解码，否则报错）
    return { ...prev, [key]: val } // 此处需要转码，否则中文和一些特殊字符就无法支持了
  }, {})
}
/**
 * 跨域文件下载
 * 跨域文件设置download会不生效。如果要保证设置的文件名起效果的话，请调用这个函数
 * 详情请看：https://blog.csdn.net/qq_41801059/article/details/125679903
 * @param {String} fileName 文件名
 * @param {String} url 请求地址
 * @举例子 crossOriginDownload('文件.xlsx', "http://192.168.10.11:48079/admin-api/infra/file/4/get/4c71bd392f26b860420330100f6f2471d3f281c6acae74d16e0b5ec60297d92f.xlsx")
 */
export async function crossOriginDownload(fileName, url) {
  // const res = await request({ method: "GET", url: url, responseType: 'blob' }, 0) // 如果采用axios请求的话
  const xhr = new XMLHttpRequest()
  xhr.open('GET', url, true)  // url 文件的完整地址 http:XXX
  xhr.responseType = 'blob'
  xhr.onload = function () {
    if (xhr.status === 200) {
      const res = xhr.response;
      const link = document.createElement('a')
      link.style.display = 'none'
      const url = window.URL.createObjectURL(res)
      link.href = url
      link.setAttribute('download', fileName)
      document.body.appendChild(link)
      link.click()
      window.URL.revokeObjectURL(url)
      document.body.removeChild(link)
    }
  }
  xhr.send()
}
/**
 * 二进制流文件下载：支持blob对象和url地址
 * @举例1 downloadFile('123123.png', 'https://xxxxxxx.png')
 * @举例2 downloadFile('123123.png', 'http://192.168.10.36:18049/open/file/download?data=M80/CELarJJQA1OgRtank6oq+/1xrY/rnMLA86dc1AAGXROW5FENy3V4MWWkNfGo')
 * @举例3 downloadFile('123123.png', Blob二进制对象) // 第二个参数是二进制流，后端返回的
 */
export async function downloadFile(fileName, pathOrBlob){
  const url = isString(pathOrBlob) ? pathOrBlob : window.URL.createObjectURL(new Blob([pathOrBlob]))
  if(isString(pathOrBlob)) { return crossOriginDownload(fileName, pathOrBlob) }
  const link = window.document.createElement('a')
  link.style.display = 'none'
  link.href = url
  link.download = fileName
  document.body.appendChild(link)
  link.click()
  !isString(pathOrBlob) && window.URL.revokeObjectURL(url)
  document.body.removeChild(link)
}
/**
 * 用户在个人中心上传头像，很多人选择高清原图导致，上传慢，服务器压力大，加载影响性能。可以通过此函数进行压缩生成新的二进制数据
 * @param {*} file 
 * @param {*} quality 
 * @returns 
 * @举例 
  input.onchange = async (e) => {
    const file = e.target.files[0]
    if (file) {
      const compressedBlob = await compressImage(file, 0.7)
      const compressedFile = new File([compressedBlob], file.name, { type: 'image/jpeg' })
      uploadToServer(compressedFile) // 上传到服务器
    }
  }
 */
export function compressImage(file, quality = 0.7) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.readAsDataURL(file)
    reader.onload = () => {
      const img = new Image()
      img.src = reader.result
      img.onload = () => {
        const canvas = document.createElement('canvas')
        const ctx = canvas.getContext('2d')
        canvas.width = img.width
        canvas.height = img.height
        ctx.drawImage(img, 0, 0, img.width, img.height)
        canvas.toBlob(blob => resolve(blob), 'image/jpeg', quality)
      }
    }
    reader.onerror = reject
  })
}
// 打开全屏
export function toFullScreen(){
  let el = document.documentElement
  let rfs = el.requestFullScreen || el.webkitRequestFullScreen || el.mozRequestFullScreen || el.msRequestFullScreen
  if(rfs) {
    rfs.call(el)
  } else if (typeof window.ActiveXObject !== "undefined") {
    //for IE，这里其实就是模拟了按下键盘的F11，使浏览器全屏
    let wscript = new ActiveXObject("WScript.Shell")
    if (wscript != null) { wscript.SendKeys("{F11}") }
  }else{
    alert("浏览器不支持全屏")
  }
}
// 退出全屏
export function exitFullscreen(){
  let el = parent.document
  let cfs = el.cancelFullScreen || el.webkitCancelFullScreen || el.mozCancelFullScreen || el.exitFullScreen
  if (cfs) {
    cfs.call(el)
  } else if (typeof window.ActiveXObject !== "undefined") {
    //for IE，这里和fullScreen相同，模拟按下F11键退出全屏
    let wscript = new ActiveXObject("WScript.Shell")
    if (wscript != null) { wscript.SendKeys("{F11}") }
  } else {
    alert("切换失败,可尝试Esc退出")
  }
}
/**
 * 返回一个lower - upper之间的随机数
 * @param {Number} lower 下限
 * @param {Number} upper 上限
 * @param {String} type 数据类型  float：浮点型    int：整型
 * @returns {Number}
 * @范围 [lower, upper)  // 请注意：左闭右开
 * @举例 random(0, 0.5) ==> 0.3567039135734613
 * @举例 random(1, 2) ===> 1.6718418553475423
 * @举例 random(-2, -1) ==> -1.4474325452361945
 * @举例 random(1, 8) ==> 6
 * 原生参考代码:  a = new Date % 100; // 两位整数随机数
 * a = new Date % 1000 // 三位整数随机数
 * a = new Date % 10000 // 四位整数随机数...依次类推
 */
export function random(lower, upper, type = 'float') {
  lower = Number(lower) || 0
  upper = Number(upper) || 0
  let res = Math.random() * (upper - lower) + lower
  if(type !== 'float') { res = Number(round(res)) }
  return res
}
/**
 * 获取随机颜色
 * @returns 
 */
export function randomColor(){
  const [r, g, b] = [addZero(random(0, 255,'int').toString(16), 2), addZero(random(0, 255,'int').toString(16), 2), addZero(random(0, 255,'int').toString(16), 2)]
  return `#${r}${g}${b}`
}
// 禁止复制
export const noCopy = () => ['contextmenu', 'selectstart', 'copy'].forEach(ev => document.addEventListener(ev, event => (event.returnValue = false)))
/**
 * 保证json格式数据的可靠获取
 * @param {Function} run 需要执行的函数
 * @param {*} defaultVal 默认值
 * @举例
  const obj = { 
    province: { 
      city: { 
        area: { 
          name: '新北区'
        },
        name:'常州市'
      },
      name: '江苏省' 
    } 
  }
 * @举例 safeGet(() => obj.province.city.area.name, '') ---> ''
 * @举例 避免了这种写法： obj.province && obj.province.city && obj.province.city.area ? obj.province.city.area.name : ''
 */
export function safeGet(run, defaultVal = '') {
  try {
    const res = run()
    return isEmpty(res) ? defaultVal : res
  } catch(e) {
    return defaultVal 
  } 
}
/*
**********************************************************************************************
******************************************金额、数字操作*********************************************
**********************************************************************************************
*/
/**
 * 将一个值转数字，如果失败就返回原始值
 * @param {Number|String} val 需要转数字的值
 * @returns 
 * @举例 toNumber('12') ----> 12
 */
export const toNumber = val => isNaN(parseFloat(val)) ? val : parseFloat(val)
/**
 * 0、1转换--------'0'、'1'转换
 * @param {Number|String} val 
 * @returns 
 * @举例子 toogle01(0) ---> 1  、  toogle01(1) ---> 0
 * @举例子 toogle01('0') ---> '1'  、  toogle01('1') ---> '0'
 */
export const toogle01 = val => isNumber(val) ? val ^ 1 : String(Number(val) ^ 1)
/**
 * 数据范围
 * @param {Number|String} num 需要限制的数字
 * @param {Number|String} min 限制最小值
 * @param {Number|String} max 限制最大值
 * @举例 range(12.23, 7, 10)  ===> 10 // 上限为10 因此返回10
 * @举例 range(12.23, 14, 20)  ===> 14 // 下限为14 因此返回14
 */
export function range(num, min = null, max = null) {
  if(min !== null) { num = Math.max(Number(num), Number(min)) }
  if(max !== null) { num = Math.min(Number(num), Number(max)) }
  return num
}
/**
 * 四舍五入返回N位有效数字（常用于金额计算）
 * @param {Number|String} num 需要处理的的数字、支持传入字符串
 * @param {Number} prec 保留的小数位数
 * @param {String} type 'keep': 小数末尾0要保留   'drop'：小数末尾0不需要保留
 * @举例 round(12.35) ==> 12  // 12.35 保留0位小数四舍五入得到 12
 * @举例 round(12.35, 1) ==> 12.4 // 12.35 保留1位小数四舍五入得到 12.4
 * @举例 round(12.5, 2) ==> 12.50 // 12.5 保留2位小数四舍五入得到 12.50
 * @举例 round(12.5, 2, 'drop') ==> 12.5 // 12.5 保留2位小数四舍五入得到 12.50---->末尾0要省去--->12.5
 */
export function round(num, prec = 0, type = 'keep') {
  prec = range(prec, 0)
  const k = Math.pow(10, prec)
  const [left = '', right = ''] = String(Math.round(Number(num) * k) / k).split('.')
  const decimal = type == 'keep' ? right + new Array(prec + 1).join('0') : right
  const decimalRight = `${prec > 0 ? '.' + decimal.slice(0, prec) : ''}`
  return `${left}${decimal.length > 0 ? decimalRight : ''}`
}
/**
 * 大数相加
 * @param {String | Number} num1 
 * @param {String | Number} num2 
 * @returns 相加后的大数
 * @举例 largeNumAdd('123000000000000000000000000010', '123000000000000000000000000009') ----> '246000000000000000000000000019'
 */
export const largeNumAdd = function (num1, num2){
  ;[num1, num2] = [String(num1), String(num2)]
  let maxLen = Math.max(num1.length, num2.length)
  const toogleNum = n => addZero(n, maxLen).split('').map(v => parseInt(v))
  ;[num1, num2] = [toogleNum(num1), toogleNum(num2)]
  const res = num1.reduceRight((prev, item, index) => {
    const figure = item + num2[index] + prev.carry
    return { sum: String(figure % 10) + prev.sum, carry: Math.floor(figure / 10) }
  }, {sum:'', carry: 0})
  return res.sum
}
/**
 * 金额处理（常用于金额计算）
 * @param {Number|String} num 要格式化的数字
 * @param {String} type float->小数形式。  intFloat->当整数的时候不需要带两个小数0，带小数时，保留几位小数   int只要整数
 * @param {Number} prec 保留几位小数
 * @param {String} sep 千分位符号
 * @举例 formatMoney(12322.1223, 'float') ====> "12,322.12" // 保留0位小数四舍五入得到 12
 * @举例 formatMoney(12322.1223, 'float', 1) ------> "12,322.1"  固定显示1位小数
 * @举例 formatMoney(12322, 'intFloat') ------> "12322"  当没有小数点就显示整数，否则显示整数
 */
export function formatMoney(num = 0, type = 'float', prec = 2, dec = '.', sep = ',') {
  num = String(num).replace(/[^0-9+-Ee.]/g, '') || '0'
  prec = Number(prec)
  let [intStr = '', floatStr = ''] = String(round(num, prec)).split(dec) // 分割出整数和小数部分
  let re = /(-?\d+)(\d{3})/ // 匹配整数部分每个三位数
  while (re.test(intStr)) { intStr = intStr.replace(re, "$1" + sep + "$2") } // 整数部分三位数添加分隔符如','
  if((type === 'intFloat' && !num.includes('.')) || num === '0' || type == 'int') { return intStr }
  floatStr += new Array(prec + 1).join('0')
  return `${intStr}${dec}${floatStr.slice(0, prec)}`
}
/*
**********************************************************************************************
******************************************日期时间操作*********************************************
**********************************************************************************************
*/
/**
 * 处理时间
 * @param {*} t 时间 
 * @returns 
 * @举例 processDate() ---->  默认返回当前时间
 * @举例 processDate('2022-10-01') ----> '2022/10/01'
 */
export function processDate(t) {
  if(!isDate(t) && isString(t) && !t.includes('T') && t.length > 0) { t = t.replace(/[-]/g, "/") }
  if(!t) { t = new Date() }
  if(isString(t) && t.replace(/[-/.]/g, '') == t && t.length == 8) {
    t = `${t.slice(0, 4)}-${t.slice(4, 6)}-${t.slice(-2)}`
  }
  return t
}
/**
 * 提取传入日期年、月、日、时、分、秒
 * @param {String|Date|Number} t 
 * @returns 
 * @举例 extract() ---->   注意：不传值默认显示当天数据，比如当前时间如 ['2022', '11', '14', '16', '46', '16']
 * @举例 extract('2022-10-01') ---->  ['2022', '10', '01', '00', '00', '00']
 * @举例 extract('2022-10-01 12:12:14') ---->  ['2022', '10', '01', '12', '12', '14']
 */
export function extract(t = new Date()){
  t = processDate(t)
  const d = new Date(new Date(t).getTime() + 8*3600*1000)
  const [year, month, day, hour, minutes, second] = new Date(d).toISOString().split(/[^0-9]/).slice(0, -1)
  return [year, month, day, hour, minutes, second]
}
/**
 * 生成格式化时间字符串
 * @param {String} formater 时间格式
 * @param {*} t 传入的时间
 * @returns 处理之后的时间数据
 * @举例 dateFormater('YYYY-MM-DD hh:mm', new Date()) ==> 2019-06-26 18:30
 * @举例 dateFormater('YYYYMMDD-hh:mm:ss', '2020-08-12 09:13:54') ==> 20200812-09:13:54
 */
export function dateFormater(formater = 'YYYY-MM-DD hh:mm:ss', t){
  if(!t) {return ''}
  t = processDate(t)
  const [Y, M, D, h, m, s] = extract(t)
  return formater.replace(/YYYY|yyyy/g, Y).replace(/YY|yy/g, Y.slice(2, 4)).replace(/MM/g, M).replace(/DD/g, D).replace(/hh/g, h).replace(/mm/g, m).replace(/ss/g, s)
}
/**
 * 获取N天后日期字符串
 * @param {Number} num 初始日期 + num天后的日期， 默认是今天 + 0天，仍然是今天
 * @param {String} split 日期分割符
 * @param t 初始日期，默认今天
 * @举例 afterNDay(0) ---> 20200904    afterNDay(1) ---> 20200905
 * @举例 分割：afterNDay(1, '-')--->2020-09-05
 */
export function afterNday(num = 0, split = '', t = new Date()) {
  if(!t) {return t}
  t = processDate(t)
  const dt = new Date(t)
  dt.setDate(dt.getDate() + Number(num)) // 获取num天后的日期
  return `0000${dt.getFullYear()}`.slice(-4) + split + `00${(dt.getMonth() + 1)}`.slice(-2) + split + `00${dt.getDate()}`.slice(-2)
}
/**
 * 获取N个月之后的日期
 * @param {String|Date} startTime  开始日期
 * @param {Number} num   N个月之后的日期
 * @returns 
 */
export function afterNMonthDate(startTime, num){
  startTime = processDate(startTime)
  var dt = new Date(startTime)
  dt.setMonth(dt.getMonth() + Number(num))
  return dateFormater('YYYY-MM-DD', dt)
}
/**
 * 计算两个日期之间间隔多少年,多少月,多少天,多少小时，多少分钟，多少秒
 * @param {String|Date} start 开始时间
 * @param {String|Date} end 结束时间
 * @returns 返回两个日期间隔多少年,多少天,多少月
 * @举例 diffTimes('2024-01-22', '2024-05-17') ----->  { "diffDay": "116", "diffYear": "0.33", "diffMonth": 4 }
 * @举例 diffTimes('2024-01', '2024-05') ----->  {"diffDay": "121","diffYear": "0.33","diffMonth": 4}
*/
export function diffTimes(start = new Date(), end = new Date()){
  ;[start, end] = [processDate(start), processDate(end)]
  const diffDay = round((new Date(end) - new Date(start)) / 1000 / 60 /60 / 24, 2)
  const [startStr, endStr] = [dateFormater('YYYYMM', start), dateFormater('YYYYMM', end)]
  const [year1, mon1, year2, mon2] = [Number(startStr.slice(0, 4)), Number(startStr.slice(-2)), Number(endStr.slice(0, 4)), Number(endStr.slice(-2))]
  const diffYear = round(((year2 - year1) * 12 + mon2 - mon1) / 12, 2)
  const diffMonth = (year2 - year1) * 12 + mon2 - mon1
  const diffSecond = new Date(end).getTime() - new Date(start).getTime()
  const diffMin = round(diffSecond / 60, 2)
  const diffHour = round(diffSecond / 60 / 60, 2)
  return { diffYear, diffMonth, diffDay, diffHour, diffMin, diffSecond }
}
/**
 * 获取时间段，业务
 * @returns 
 * 昨天起止时间、今天的起止时间、上周的起止时间、当前周的起止时间、当前是星期几 ---->  带有时分秒的截止时间
 * @举例
 * socketTime()
 * ----->
 * {
 *   curWeek: ['2022-07-25', '2022-07-31'], // 本周
 *   lastWeek: ['2022-07-18', '2022-07-24'], // 上周
 *   nextWeek：['2022-08-01', '2022-08-07'], // 下一周
 *   today: "2022-07-30", // 今天
 *   week: 6, // 周几
 *   weekDay: "星期六", // 周几中文
 *   yesterday: "2022-07-30", // 昨天
 *   _curWeek: ['2022-07-25 00:00:00', '2022-07-31 23:59:59'], // 本周
 *   _lastWeek:  ['2022-07-18 00:00:00', '2022-07-24 23:59:59'], // 上周
 *   _nextWeek：['', '']
 *   _today: ['2022-07-30 00:00:00', '2022-07-30 23:59:59'], // 今天
 *   _yesterday: ['2022-07-30 00:00:00', '2022-07-30 23:59:59'], // 昨天
 * }
 */
export function socketTime(t = new Date()) {
  t = processDate(t)
  const [year, month, day, hour, minutes, seconds] = extract(t)
  const dt = new Date(t)
  const curSecond = dt.getTime()
  const [_month, _day] = [String(dt.getMonth() + 1), String(dt.getDate())]
  const [week, daySeconds] = [dt.getDay(), 1000 * 60 * 60 * 24]
  const minusDay = week !== 0 ? week - 1 : 6
  const weekDay = `星期${"日一二三四五六"[week]}`
  const [dateStr, startStr, endStr, yesterDayStart] = ['YYYY-MM-DD', 'YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59', curSecond - daySeconds]
  const [yesterday, _yesterday] = [dateFormater(dateStr, yesterDayStart), [dateFormater(startStr, yesterDayStart), dateFormater(endStr, yesterDayStart)]] // 昨天
  const [today, _today] = [dateFormater(dateStr, dt), [dateFormater(startStr, dt), dateFormater(endStr, dt)]] // 今天
  // 上周
  const [lastWeekStart, lastWeekEnd] = [curSecond - (minusDay + 7) * daySeconds, curSecond - (minusDay + 1) * daySeconds]
  const [lastWeek, _lastWeek] = [[dateFormater(dateStr, lastWeekStart), dateFormater(dateStr, lastWeekEnd)], [dateFormater(startStr, lastWeekStart), dateFormater(endStr, lastWeekEnd)]]
  // 本周
  const [curWeekStart, curWeekEnd] = [curSecond - minusDay * daySeconds, curSecond + (6 - minusDay) * daySeconds]
  const [curWeek, _curWeek] = [[dateFormater(dateStr, curWeekStart), dateFormater(dateStr, curWeekEnd)], [dateFormater(startStr, curWeekStart), dateFormater(endStr, curWeekEnd)]]
  // 下周
  const nextWeekStart = curSecond + (8 - week) * daySeconds
  const nextWeekEnd = nextWeekStart + 6 * daySeconds
  const [nextWeek, _nextWeek] = [[dateFormater(dateStr, nextWeekStart), dateFormater(dateStr, nextWeekEnd)], [dateFormater(startStr, nextWeekStart), dateFormater(endStr, nextWeekEnd)]]
  return { year, _month, month, day, _day, hour, minutes, seconds, yesterday, _yesterday, today, _today, lastWeek, _lastWeek, curWeek, _curWeek, week, weekDay, nextWeek, _nextWeek }
}
/**得到当前时间之后N秒的时间
 * @param {Number} after 多少秒之后的时间
 * @举例 afterNsecond(20)  // 20s之后的时间
 */
export function afterNsecond(after = 60) {
  const dt = new Date().getTime() + after * 1000
  return new Date(dt)
}
/**
 * 将毫秒数转换成天、时、分、秒、毫秒
 * @param {String} formater 时间格式
 * @param {Number} leftMs 剩余的时间，毫秒数
 * @param {Number} strType  ellipsis: 当出现 “0天1小时” 显示为 “1小时”
 * @returns 
 * @举例
 * ms2Dhs('dd天hh小时mm分钟ss秒', 62e3)
 * --->
 * {formateStr: '01分钟02秒', d: 0, h: '00', m: '01', s: '02', ms: '500'}
 */
export function ms2Dhs(formater = 'dd天hh小时mm分钟ss秒', leftMs, strType = 'ellipsis') {
  const minute = leftMs / 1000 / 60
  let [d, h, m, s, ms] = [Math.floor(minute / 60 / 24), Math.floor(minute / 60 % 24), Math.floor(minute % 60), Math.floor(leftMs / 1000 % 60), Math.floor(leftMs % 1000)]
  if(strType == 'ellipsis') {
    let regStr = d > 0 ? 'dd' : h > 0 ? 'hh' : m > 0 ? 'mm' : s > 0 ? 'ss' : ms > 0 ? 'ms' : ''
    formater = formater.slice(formater.indexOf(regStr))
  }
  ;[h, m, s, ms] = [addZero(h, 2), addZero(m, 2), addZero(s, 2), addZero(ms, 3)]
  return formater.replace(/dd/g, d).replace(/hh/g, h).replace(/mm/g, m).replace(/ss/g, s).replace(/ms/g, ms)
}
/**
 * 根据年和月，得出该年月有多少天。（原理：计算出他的下个月， 用它的下个月生成毫秒数-当前月毫秒数再除以一天的毫秒数===天数）
 * @param {String} year 
 * @param {String} month 
 * @returns 
 * @举例子 getDays(2021, 11) ---> 30
 */
export function getDays(year, month) {
  let nextMoth = Number(month) + 1 == 13 ? 1 : Number(month) + 1
  let nextYear = Number(month) + 1 == 13 ? Number(year) + 1 : Number(year)
  let milliseconds = new Date(`${nextYear}-${nextMoth}-1`).getTime() - new Date(`${year}-${month}-1`).getTime()
  let daymilliseconds = 3600 * 24 * 1000
  return milliseconds / daymilliseconds
}
/**
 * 日期一和日期二之间的间隔的天数
 * @param {String|Date|Number} date1 日期一
 * @param {String|Date|Number} date2 日期二
 * @returns 
 * @举例 dayDiff("2021-11-3", "2022-2-1") ----> 90
 */
export const dayDiff = (date1, date2) => Math.ceil(Math.abs(new Date(date1.replace(/[-]/g, "/")).getTime() - new Date(date2.replace(/[-]/g, "/")).getTime()) / 86400000) // 86400 === 24 * 60 * 60 秒
/**
 * 查出日期位于一年中的第多少天
 * @param {Date || String || Number} date 传入日期
 * @returns 传入的日期是一年中的第多少天
 * @举例 dayOfYear(new Date()) ----> 307
 */
export const dayOfYear = date => Math.floor((date - new Date(date.slice(0, 4), 0, 0)) / 1000 / 60 / 60 / 24)
/*
**********************************************************************************************
******************************************正则校验*********************************************
**********************************************************************************************
*/
/**正则校验返回true || false
 * @param {String} val 需要正则校验的值
 * @param {reg} reg 正则表达式
 * @returns {Boolean}
 */
export const regTest = (val, reg) => new RegExp(reg).test(val)
/*
**********************************************************************************************
******************************************温度转换*********************************************
**********************************************************************************************
*/
/**
 * 温度转换  摄氏度  ---> 华氏度
 * @param {Number} 摄氏度
 * @returns 华氏度
 */
export const c2f = celsius => celsius * 9 / 5 + 32
/**
 * 温度转换 华氏度 ----> 摄氏度
 * @param {Number} 华氏度
 * @returns 摄氏度
 */
export const f2s = fahrenheit => (fahrenheit - 32) * 5 / 9
/*
**********************************************************************************************
******************************************业务函数*********************************************
**********************************************************************************************
*/
/**
 * 设置接口字段默认值
 * @param {*} res 接口返回的数据
 * @param {*} defaultObj 默认值对象，可以选填一些关键字段。
 * @returns 
 * @举例
 * const res = setDef({ list: null, detail: {}, name: 'tom' }, {list: [], detail: { arr: []}}) // 这样就对原始数据进行了默认值处理
  {
    "list": [],
    "detail": {
      "arr": []
    },
    "name": "tom"
  }
 */
export function setDef(res = {}, defaultObj = {}) {
  return Object.entries(defaultObj).reduce((prev, [key, val]) => {
    if(typeStr(prev[key]) !== typeStr(val) || isEmpty(val)) {
      console.warn(`默认值兼容处理:${key}-${JSON.stringify(prev[key])}`)
      prev[key] = val
    }
    return prev
  }, res)
}
/**
 * 字段平铺，将某一些类型的字段进行平铺
 * @param {Object} obj 需要处理的数据
 * @param {String} fn () => true 这代表全部解析。  k => ['school'].includes(k) 代表只解析school字段
 * @param {*} result 最终结果
 * @param {*} prevKey 前置字段，用于递归的时候，传给子递归处理
 * @param {*} split 平铺后，每个字段的分隔符
 * @returns 
 * @举例
  flatKey({
   name: '', 
   age: 18, 
   school: {
     address: '江苏省常州市武进区', 
     name:'石桥小学'
   },
   teacher: ['王老师', '李老师']
  })
  ------>
  {
    "name": "",
    "age": 18,
    "school__address": "江苏省常州市武进区",
    "school__name": "石桥小学",
    "teacher__0": "王老师",
    "teacher__1": "李老师"
  }
 */
export function flatKey(obj, fn = () => true, result = {}, prevKey = '', split = '__'){
  const parseObj = (k, v) => isFunction(fn) && fn(k, v) && isObject(v) // 校验是否是对象类型，并且是平铺字段
  const parseArr = (k, v) => isFunction(fn) && fn(k, v) && isArray(v) // 校验是否是数组类型，并且是平铺字段
  const getKey = val => val.filter(v => v !== '').join(split) // 获取拼接字段
  Object.entries(obj).forEach(([key, val]) => {
    if(parseObj(key, val)) {
      result = flatKey(val, fn, result, getKey([prevKey, key]))
    } else if(parseArr(key, val)) {
      val.forEach((v, i) => { result[getKey([prevKey, key, i])] = v })
    } else {
      result[getKey([prevKey, key])] = val
    }
  })
  while(Object.entries(result).some(([key, val]) => (parseObj(key, val)) || (parseArr(key, val)))) {
    const [todoResult, finishResult] = [{}, {}]
    Object.entries(result).forEach(([key, val]) => (parseObj(key, val) || parseArr(key, val) ? todoResult : finishResult)[key] = val)
    result = flatKey(todoResult, fn, finishResult)
  }
  return result
}
/**
 * 将平铺的字段，收起来
 * @param {*} obj 
 * @returns 
 * @举例
 * unFlatKey({
    "name": "",
    "age": 18,
    "school__address": "江苏省常州市武进区",
    "school__name": "石桥小学",
    "teacher__0": "王老师",
    "teacher__1": "李老师"
  })
  ----------->
  {
    "name": "",
    "age": 18,
    "school": {
      "address": "江苏省常州市武进区",
      "name": "石桥小学"
    },
    "teacher": ["王老师","李老师"]
  }
 */
export function unFlatKey(flatObj, split = '__') {
  const result = {}
  Object.entries(flatObj).forEach(([key, value]) => {
    const keys = key.split(split)
    let current = result
    keys.forEach((k, index) => {
      if (!current[k]) {
        current[k] = isNaN(keys[index + 1]) ? (index === keys.length - 1 ? value : {}) : []
      }
      if (isArray(current[k]) && index !== keys.length - 1) {
        const nextKey = Number(keys[index + 1])
        if (!current[k][nextKey]) {
          current[k][nextKey] = index === keys.length - 2 ? value : {}
        }
        current = current[k][nextKey]
      } else {
        current = current[k]
      }
    })
  })
  return result
}
/**
 * 获取平铺后的全部图片url地址
 * @param {Object} obj 
 * @returns 
 */
export function getImgs(obj) {
  const allField = flatKey(obj, () => true)
  const allImgUrls = Object.keys(allField).map(v => allField[v]).filter(v => isString(v) && v.includes('http') && (v.includes('.png') || v.includes('.jpg') || v.includes('.jepg') || v.includes('.bmp') || v.includes('.tiff') || v.includes('data:image/')))
  return [...new Set(allImgUrls)]
}
/**
 * 图片url地址转base64格式，从而解决跨域问题
 * @param {String} url 需要进行转换的url地址
 * @param {String} type 'all'：代表需要返回文件后缀  'data':代表只返回base图片字符串
 * @returns 
 */
export function convertUrlToBase64(url, type = 'data') {
  return new Promise((resolve) => {
    let img = new Image()
    img.crossOrigin = 'Anonymous'
    img.src = url
    img.onload = function () {
      let canvas = document.createElement('canvas')
      canvas.width = img.width
      canvas.height = img.height
      let ctx = canvas.getContext('2d')
      ctx.drawImage(img, 0, 0, img.width, img.height)
      let ext = img.src.substring(img.src.lastIndexOf('.') + 1).toLowerCase()
      let dataURL = canvas.toDataURL('image/' + ext)
      let base64 = { dataURL, type: 'image/' + ext, ext }
      resolve(type == 'data' ? base64.dataURL : base64 )
    }
  })
}
/**
 * 应用于当前DOM元素，将DOM滚动到指定位置
 * 整个页面之间，平滑滚动
 * --->取代锚点。将传入的DOM，滚动到指定位置
 * @param {DOM} dom 传入的DOM元素
 * @param {Number} position 滚动到什么位置  'top'：滚动到视口的顶部   'center'：滚到到视口中央  'bottom'：滚动到视口底部  'noscroll'：不滚动
 * @param {Number} type 滚动类型：'smooth'：平滑滚动   'instant'：迅速滚动
 * @举例子 domScroll(this.$refs.testRef, 'bottom', 'smooth') // 执行完这个函数，那么会找到页面上ref为testRef的DOM，然后平滑滚动到页面顶部
 */
// export const domScroll = function (dom, position = 'top', type = 'smooth') {
//   const mapPosition = ['start', 'center', 'end', 'auto']
//   dom.scrollIntoView({ behavior: type, block: mapPosition[position] })
// }
/**
 * 滚动的盒子中的某个位置。可以是任意一个盒子
 * 只要盒子是 超出滚动的，拿到他的dom，然后，传入滚动距离必然可以滚动
 * 注意，如果想让这个盒子能滚动到某个位置。首先这个盒子必须是可滚动的，也就是高度得固定的
 * @param {String|dom} 盒子的id 或者这个盒子的dom
 * @param {Number} offsetHeight 需要滚动的距离
 * @param {String} type 滚动类型 'smooth'：平滑滚动   'instant'：迅速滚动
 * @param {String} direction 滚动方向 'y'：垂直滚动   'x'：水平滚动
 * @returns
 * @举例子 boxScroll('xxxid', 300)
 */
export function boxScroll(e, offsetHeight = 0, type = 'smooth', direction = "y") {
  const params = direction == 'y' ? { behavior: type, top: offsetHeight } : { behavior: type, left: offsetHeight }
  const dom = isDom(e) ? e : getDom(e)
  dom && dom.scrollTo(params) // 平滑滚动 | 迅速滚动
}
// 获取视口总高度
export const get100vh = () => window.innerHeight || document.body.clientHeight || document.documentElement.clientHeight
// 获取视口总宽度
export const get100vw = () => window.innerWidth || document.body.clientWidth || document.documentElement.clientWidth
// 获取当前滚动距离顶部的距离
export const getScrollTop = () => (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop
/**
 * 获取元素当前所在位置
 * 距离视窗的距离。一般现在通过 IntersectionObserver API实现了，请看https://www.ruanyifeng.com/blog/2016/11/intersectionobserver_api.html
 * @param {Dom} e id 或者是直接传入dom
 * @returns 
 * @举例 getViewPos(this.$refs.xxx)   ----> { "top": 60, "bottom": 60, "left": 0, "right": 1477, "width": 200, "height": 50 }
 * @举例 getViewPos('id')   ----> { "top": 60, "bottom": 60, "left": 0, "right": 1477, "width": 200, "height": 50 }
 * @举例 getViewPos(_.getDom('id'))   ----> { "top": 60, "bottom": 60, "left": 0, "right": 1477, "width": 200, "height": 50 }
 */
export function getViewPos(e, equal = false) {
  const dom = isDom(e) ? e : getDom(e)
  if(equal) { // 无误差情况
    const styles = window.getComputedStyle(dom)
    const {top, bottom, left, right, width, height } = styles
    const getNum = str => safeGet(() => Number(str.match(/(\d+)/)[1]), str)
    return { top: getNum(top), bottom: getNum(bottom), left: getNum(left), right: getNum(right), width: e.offsetWidth, height: e.offsetHeight, styles }
  } else { // 在有translate动画的时候，e.getBoundingClientRect()计算会存在误差的。
    let {top, bottom, left, right, width, height } = dom.getBoundingClientRect()
    const { clientTop = 0, clientLeft = 0 } = window.document.documentElement // html元素对象的上边框的上边距和左边距
    ;[top, bottom, left, right] = [top - clientTop, bottom - clientTop, left - clientLeft, right - clientLeft]
    const {scrollTop = 0, scrollHeight = 0} = dom
    // height: dom盒子高度   scrollTop滚动的距离       scrollHeight：如果没有滚动的话，全部展开多高。（height + 最大scrollTop === scrollHeight）
    return { top, bottom, left, right, width, height, scrollTop, scrollHeight } // 是否触底允许有2px的误差，提高用户体验
  }
}
/**
 * 浏览器下一帧渲染之前执行此函数
 * @param {Function} fn 需要在下一次事件循环执行的函数
 * @举例 nextTick(() => {console.log('123')})
 */
export const nextTick = fn => window.requestAnimationFrame(fn)
/**
 * 轻提示
 * @param {String} str 提示的字符串内容
 * @param {Number} time 提示显示的时间
 * @param {Number} type 显示的样式类型
 * @举例 showToast('请输入手机号码')  // 弹出“请输入手机号码”这个提示，并且1500ms后自动消失
 */
export function showToast(str, time = 1500, type = 0) {
  var pObj = document.createElement("div") // 创建，写内容
  const innerHTML = {
    0: `<div class="nowrap1" style="position:fixed;z-index:9999;top:45%;left:50%;transform: translateX(-50%);font-size:0.30rem;padding:0.2rem 0.5rem;background:#4A4A4A;color:#fff;border-radius:0.15rem;min-width:3.8rem;text-align:center;">${str}</div>`,
    1: `<div class="nowrap1" style="position:fixed;z-index:9999;top:45%;left:50%;transform: translateX(-50%);font-size:0.30rem;padding:0.2rem 0.5rem;background:#fff;color:#000;border-radius:0.15rem;min-width:3.8rem;text-align:center;">${str}</div>`,
  }
  pObj.innerHTML = innerHTML[type] //添加内容
  document.body.appendChild(pObj)
  setTimeout(() => document.body.removeChild(pObj), time)
}
/**
 * 删除css结点
 * @param {String|dom} e 需要删除的结点的id或者dom
 * @举例 removeCss('z-loading-style')  // 删除id为z-loading-style的css结点
 */
export function removeCss(e) {
  const selectDom = isDom(e) ? e : getDom(e)
  if(selectDom){ document.getElementsByTagName('head').item(0).removeChild(selectDom) } // 清除样式
}
/**
 * 往网页头部动态追加css
 * @param {String} css 可以手动传入需要载入的样式
 * @param {String} id 这个css的id，方便以后进行删除操作
 * @举例 addCss('@keyframes moveY {0%{transform: translateY(0%);}100%{transform: translateY(-100%);}}', 'z-loading-style')  // 载入移动动画样式
 */
export function addCss(css = '', id = ""){
  removeCss(id) // 删除上次添加的这个id的CSS
  var styleObj = document.createElement('style')
  styleObj.id = id
  styleObj.innerHTML= css
  document.getElementsByTagName('head').item(0).appendChild(styleObj) // 添加样式到头部
  // document.head.append(styleObj)
}
/**
 * 删除DOM结点
 * @param {String|dom} id 需要删除的结点的id或者dom
 * @举例 removeDom('z-loading')  // 删除id为z-loading的dom
 */
export function removeDom(e) {
  const selectDom = isDom(e) ? e : getDom(e)
  if(selectDom){ document.body.removeChild(selectDom) } // 清除DOM
}
/**
 * 往网页头部动态追加Dom
 * @param {Dom} dom 可以手动传入需要载入的Dom
 * @param {String} id 这个css的id，方便以后进行删除操作
 * @举例 addDom('<div>234324</div>', 'z-loading')  // 载入的dom
 */
export function addDom(dom = '', id = ""){
  removeDom(id) // 删除上次添加的这个id的DOM
  var divObj = document.createElement("div")
  divObj.id = id
  divObj.innerHTML = dom
  document.body.appendChild(divObj) // 添加Dom节点到body中
  return divObj
}
/**
 * 添加类名
 * @param {Dom} el dom对象
 * @param {String} cls 样式类名样式类名
 * @returns 
 * @举例 _.addClass(_.query('#test'), 'fs26 b') ----> 为id为test的盒子，添加fs26 b这两个样式
 */
export function addClass(el, cls) {
  if (!cls || !(cls = cls.trim())) { return }
  if (el.classList) { return cls.indexOf(' ') > -1 ? cls.split(/\s+/).forEach(c => el.classList.add(c)) : el.classList.add(cls) }
  var cur = " ".concat(el.getAttribute('class') || '', " ")
  cur.indexOf(' ' + cls + ' ') < 0 && el.setAttribute('class', (cur + cls).trim())
}
/**
 * 删除类名
 * @param {Dom} el dom节点
 * @param {String} cls 样式类名
 * @returns 
 * @举例  _.removeClass(_.query('#test'), 'b') ---->  为id为test的盒子，移除b这个样式
 */
export function removeClass(el, cls) {
  if (!cls || !(cls = cls.trim())) { return }
  if (el.classList) {
    cls.indexOf(' ') > -1 ? cls.split(/\s+/).forEach(c => el.classList.remove(c)) : el.classList.remove(cls)
    !el.classList.length && el.removeAttribute('class')
  } else {
    var cur = " ".concat(el.getAttribute('class') || '', " ")
    var tar = ' ' + cls + ' '
    while (cur.indexOf(tar) >= 0) { cur = cur.replace(tar, ' ')}
    cur = cur.trim()
    cur ? el.setAttribute('class', cur) : el.removeAttribute('class')
  }
}
/**
 * 创建节点
 * @param {*} tagName 标签名称
 * @returns
 * @举例  createElement('div')
 */
export const createElement = tagName => document.createElement(tagName)
/**
 * 给指定dom添加子节点
 * @param {Dom} parent 父dom
 * @param {Dom} child 子dom
 * @举例  appendChild(_.query('#test'), createElement('div'))
 */
export const appendChild = (parent, child) => parent.appendChild(child)
/**
 * 从父节点移除子节点
 * @param {Dom} parent 父dom
 * @param {Dom} child 子dom
 * @举例  removeChild(_.query('#test'), _.query('#child'))
 */
export const removeChild = (parent, child) => parent.removeChild(child)
/**
 * 窗口调整
 * @param {Function} fn 
 */
export const resize = fn => window.onresize = () => { fn() }
/**
 * 显示DOM
 * @param {String|dom} e dom或者id
 * @举例 show('z-loading')  // 显示id为z-loading的dom结点
 */
export function show(e){
  const selectDom = isDom(e) ? e : getDom(e)
  if(selectDom){ selectDom.style.display = 'block' }
}
/**
 * 删除DOM结点
 * @param {String|dom} e 需要隐藏的结点的dom或者id
 * @举例 hide('z-loading')  // 隐藏id为z-loading的dom结点
 */
export function hide(e){
  const selectDom = isDom(e) ? e : getDom(e)
  if(selectDom){ selectDom.style.display = 'none' }
}
/**
 * 开启loading
 * @param {String} str 提示的字符串内容
 * @param {String} type loading类型   0：默认是蓝色转圈圈
 * @param {Dom} dom 可以手动传入需要加载的dom结构
 * @param {String} css 可以手动传入需要加载的样式
 * @举例 showLoading()  // 显示出loading转圈圈动画
 * 备注：次函数可以扩展，未来可以支持多种不同的loading，只要传入不同的type值就可以
 */
export function showLoading({str='加载中...', type = 0, dom ='', css = ''} = {}) {
  const domObj = {
    0: `<div style="position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;background:rgba(255,255,255)">
      <div style="position:fixed;top:30%;left:50%;transform:transLateX(-50%);width:150px;text-align:center;">
        <svg style="width:60px;height:60px;animation: rotate360 2s linear infinite;">
          <circle cx="30" cy="30" r="20" fill="none" style="color:#1989FA;animation: loading-dash 1.5s ease-in-out infinite;stroke-dasharray: 90 120;stroke-dashoffset: 0;stroke-width: 2;stroke:#1989FA;stroke-linecap: round;"></circle>
        </svg>
        <div style="color:#0094FF;text-align:center;font-size:0.3rem;font-weight:700;">${str}</div>
      </div>
    </div>`
  }
  const cssObj = {
    0: `
    @keyframes rotate360 {
      0% {transform: rotate(0);}
      100% {transform: rotate(360deg);}
    }
    @keyframes loading-dash {
      0% { stroke-dasharray: 1 200; stroke-dashoffset: 0; }
      50% { stroke-dasharray: 90 150; stroke-dashoffset: -40px; }
      100% { stroke-dasharray: 90 150; stroke-dashoffset: -120px; }
    }`
  }
  const domStr = dom || domObj[type]
  const cssStr = css || cssObj[type]
  addCss(cssStr, 'z-loading-style') // 创建样式
  addDom(domStr, 'z-loading-dom') // 创建dom
}
/**
 * 关闭loading
 * @举例 hideLoading()  // 关闭loading转圈圈动画（将DOM和样式移除了）
 */
export function hideLoading(){
  removeCss('z-loading-style')
  removeDom('z-loading-dom')
}
/**
 * 设置标题
 * @param {String} title 网页标题
 * @举例 setTitle('订餐管理系统')  // 设置首页标题为“订餐管理系统”
 */
export const setTitle = title => document.title = title
 /**
  * 跳转
  * @param {String} href 链接地址
  * @举例 goUrl('https://www.baidu.com')  // 跳转到百度
  */
export const goUrl = href => window.location.href = href
/**
 * 密码强度检测。备注：这个函数要依据不同的项目的密码强度规则，做对应的改造
 * @param {String} str 密码
 * @returns {Number} 密码强度
 * @举例 checkPwd('ss142152') ------>  2
 */
export function checkPwd(str) {
  var Lv = 0
  if (str.length < 6) { return Lv } 
  if (/[0-9]/.test(str)) { Lv++ } // 数字+1
  if (/[a-z]/.test(str)) { Lv++ } // 小写字母+1
  if (/[A-Z]/.test(str)) { Lv++ }  // 大写字母+1
  if (/[\.|-|_!@#$%^&*()`]/.test(str)) { Lv++ } // 特殊字符+1
  return Lv
}
/**
 * 异步加载js
 * @param {String} url 需要加载的js
 * @举例子 在async函数中调用 await loadJs("//res.wx.qq.com/open/js/jweixin-1.6.0.js") 
 */
export const loadJs = (function() {
  let loadedJs = []
  return function (url){
    return new Promise((resolve, reject) => {
      if(loadedJs.includes(url)) { return resolve() }
      let script = document.createElement("script")
      script.type = "text/javascript"
      script.src = url
      document.body.appendChild(script)
      script.onload = () => { loadedJs.push(url);resolve(); }
      script.onerror = () => reject()
    })
  }
})()
/**
 * 异步加载css
 * @param {String} href 需要加载的css
 * @举例子 在async函数中调用 await loadCss("") 
 */
export const loadCss = (function(href) {
  let loadedCss = []
  return function (){
    return new Promise((resolve, reject) => {
      if(loadedCss.includes(href)) { return resolve() }
      let link = document.createElement('link')
      link.setAttribute('rel', 'stylesheet')
      link.href = href
      document.head.appendChild(link)
      link.onload = () => { loadedCss.push(href);resolve(); }
      link.onerror = () => reject()
    })
  }
})()
/**
 * 图片预加载
 * @param {Array} arr 需要预加载的图片数组
 * 利用浏览器的缓存策略只要加载过的图片会缓存起来，第二次加载会优先从缓存中找
 * @举例 
 * preLoadImg([
 *   'https://health.gagctv.com/wechat/jjzs/static/wechat_icon.png',
 *   'https://health.gagctv.com/wechat/jjzs/static/basic_info_top_bg.png',
 *   'https://health.gagctv.com/wechat/jjzs/static/summary.png',
 *   'https://health.gagctv.com/wechat/jjzs/static/man_model.png',
 *   'https://health.gagctv.com/wechat/jjzs/static/person_icon.png'
 * ])
 */
export function preLoadImg(arr = []) {
  const promiseAll = arr.map(item => {
    return new Promise((resolve, reject) => {
      var img = new Image()
      img.onload = () => { img.onload = null;resolve(img) }
      img.error = () => reject('图片加载失败')
      img.src = item
    })
  })
  return Promise.all(promiseAll)
}
/**
 * 加载图片。如果传入了宽高。那么原始图片
 * 1、图片 naturalWidth > naturalHeight ---->  图片 width ---> 设定的值。高度自适应
 * 2、图片 naturalHeight > naturalWidth ---->  图片 height ---> 设定的值。宽度自适应
 * 逻辑：
 * 如果原始尺寸就是宽 >= 高，则图片显示宽度，等于传入宽度，然后自动算出图片显示高度
 * 如果原始尺寸就是宽 < 高，则图片显示高度，等于传入高度，然后自动算出图片显示宽度
 * @param {*} src 
 * @param {*} width 最大宽度边界
 * @param {*} height 最大高度边界
 * @param {*} type 适配类型   auto自动适配，如果原始图片宽>高，就限制宽为传入的宽，如果高>宽大就限制高为传入的高
 *                           width限制宽为传入的宽
 *                           height限制宽为传入的宽
 * @returns 
 */
export function loadImgObj(src, width, height, type = 'auto') {
  if(!src) { return }
  return new Promise((resolve, reject) => {
    const image = new Image()
    image.crossOrigin = "anonymous"
    image.src = src
    image.onload = e => {
      const { naturalWidth, naturalHeight } = image
      const ratio = naturalHeight / naturalWidth
      if(type == 'auto') {
        if(naturalHeight >= naturalWidth) {
          width = Number(round(height / ratio, 0))
        } else {
          height = Number(round(width * ratio, 0))
        }
      } else if(type == 'width') {
        height = Number(round(width * ratio, 0))
      } else if(type == 'height'){
        width = Number(round(height / ratio, 0))
      }
      resolve({ image: src, naturalWidth, naturalHeight, width, height, ratio, scale: width / naturalWidth   })
    }
    image.onerror = () => reject(new Error('图片加载失败'))
  })
}
/**
 * 异常捕获处理
 * @param {*} fn 
 * @param {*} catchFn 
 * @param {*} finallyFn 
 */
export async function tryCatch(fn, catchFn = e => {}, finallyFn = () => {}){
  let res = null
  try {
    res = await fn()
  } catch (e) {
    catchFn(e)
  } finally {
    finallyFn()
  }
  return res
}
/**
 * 高级定时器，首次无延迟执行，执行异步任务的时候，严格保持设定的时间间隔。随时可停止和重新开始
 * @param {*} fn 
 * @param {*} time 
 * @returns 
 * @举例 
  const { start, stop } = highInterval(async () => {
    console.log('开始请求')
    const res = await new Promise((resolve, reject) => {
      const randomError = Math.random() < 0.5
      setTimeout(() => {
        if (randomError) {
          reject("请求失败")
        } else {
          resolve("请求成功")
        }
      }, 2000);
    })
    console.log(`${res}请求完成，等待下次请求...`)
  }, 5000)
  setTimeout(() => stop(), 15000)
  setTimeout(() => start(), 25000)
 */
export function highInterval(fn, time, immediate = true) {
  let [isFetching, isRunning, timer] = [false, false, null]
  function loop() {
    if (!isRunning || isFetching) return
    isFetching = true
    timer = setTimeout(async () => await tryCatch(fn, e => console.error("请求失败:", e), () => { isFetching = false;isRunning && loop() }), time)
  }
  async function start() {
    if (!isRunning) {
      isRunning = true
      if(immediate) { await tryCatch(fn, e => console.error("请求失败:", e)) }
      loop()
    }
  }
  function stop() {
    isRunning = false
    isFetching = false
    clearTimeout(timer)
  }
  start()
  return { start, stop }
}
/**
 * 执行此函数，可以做一个延时功能。在需要延时执行一段逻辑的时候可以使用
 * @param {String|Number} t
 * @returns 返回一个promise对象，等待t时间后，最终返回定时器id
 * 举例子: await wait(500)   // 那么程序会在此处阻塞等待500ms
 * 举例子: await wait('500ms') // 那么程序会在此处阻塞等待500ms
 * 举例子: await wait('0.5s') // 那么程序会在此处阻塞等待500ms
 */
export function wait(t) {
  if(isString(t)) { t = eval(t.replace('ms', '').replace('s', '*1000')) }
  return new Promise(resolve => {const timeId = setTimeout(() => resolve(timeId), Number(t))})
}
/**
 * 深拷贝
 * @param {*} obj 传入任意类型都可以做深拷贝 
 * @returns 返回深拷贝的数据
 * @举例子
 * const obj = {name:'a', age:'18'}; 
 * deepCopy(obj) ----> {name:'a', age:'18'}
 */
export function deepCopy (obj) {
  if(!isReference(obj)) { return obj }  // 数字、日期、正则、函数、字符串、undefined、null、Symbol直接返回
  return Object.keys(obj).reduce((prev, item) => (prev[item] = isReference(obj[item]) ? deepCopy(obj[item]) : obj[item], prev), isArray(obj) ? [] : {})
}
/**
 * 获取唯一ID。用于模板渲染的唯一key值
 * @returns 
 * @举例
 * [{name:'a'}, {name:'b'}].map(v => ({...v, _id: guID()}))
 * ---->
 * [{name:'a', _id: '1vc49wwugp3400'}, {name:'b', _id:'4vvfl6wivx4000'}]
 */
export function guID() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
    const xVal = Math.random() * 16 | 0
    const yVal = xVal & 0x3 | 0x8
    return (c === 'x' ? xVal : yVal).toString(16)
  })
}
/**
 * 函数防抖
 * @param {*} fn 需要防抖的函数
 * @param {*} wait 防抖时间
 * @returns 
 * @举例 const fn = (e) => {console.log(e)}
 * @举例 const newFn = debounce(fn, 2e3) ----> 这样的话执行newFunc()就会有防抖效果
 * newFn(1);newFn(1);newFn(1);newFn(1);newFn(1);newFn(1);
 */
export function debounce(fn, wait=3e3) {
  let timeout = null  // 使用闭包，让每次调用时点击定时器状态不丢失
  return function () { 
    clearTimeout(timeout) // 如果用户在定时器（上一次操作）执行前再次点击，那么上一次操作将被取消
    timeout = setTimeout(()=> fn(...arguments), wait)
  }
}
/**
 * 函数节流
 * @param {Function} fn  需要做节流的函数
 * @param {Number|String} wait 节流时间
 * @returns 
 * @举例 const fn = () => {console.log(1)}
 * @举例 const newFn = throttling(fn, 2e3) ----> 生成了节流函数
 */
export function throttling(fn, wait=3e3) {
  let timeout = null // 使用闭包，让每次调用时点击定时器状态不丢失
  let start = +new Date() // 记录第一次点击时的时间戳
  return function () {
    clearTimeout(timeout)
    let end = +new Date() // 记录第一次以后的每次点击的时间戳
    if (end - start >= wait) { // 当时间到达设置的延时时间则立即调用数据处理函数
      fn(...arguments)
      start = end // 执行处理函数后，将结束时间设置为开始时间，重新开始记时
    } else {
      timeout = setTimeout(() => fn(...arguments), wait) // 后续点击没有到达设置的延时，定时器设定延时进行调用
    }
  }
}
/**
 * 获取cookie
 * @param {*} name cookie的字段名
 * @returns 
 * @举例 var third_app_token = getCookie('third_app_token')
 */
export function getCookie(name) {
  var arr, reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)")
  return arr = document.cookie.match(reg) ? unescape(arr[2]) : null
}
//  清除cookie
export const clearCookies = () => document.cookie.split(';').forEach(cookie => document.cookie = cookie.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date(0).toUTCString()};path=/`))
/**
 * 选择器查询
 * @param {String} selector 
 * @returns 返回全部/单个dom
 * 举例子: queryAll('.lazyLoadClass') ---->  查出所有类名为.lazyLoadClass的dom集合并转成数组
 * 举例子: query('.lazyLoadClass') ---->  查出第一个类名为.lazyLoadClass的dom
 */
export const queryAll = selector => Array.from(document.querySelectorAll(selector))
export const query = selector => document.querySelector(selector)
/**
 * 根据id获取dom
 * @param {*} id 
 * @returns 
 */
export const getDom = id => document.getElementById(id)
/**
 * 本地存储
 * @param {String} name 
 * @returns 
 * getLocalStorage('token') ----> 获取token的值比如说为："be59044c178640c6be6d7fbcff2148af"
 * setLocalStorage('token', "asdasdaasd") ----> 设置token的值为asdasdaasd
 * removeLocalStorage('token') // 移除token
 */
export const getLocalStorage = name => safeGet(() => JSON.parse(window.localStorage.getItem(name)), window.localStorage.getItem(name))
export const setLocalStorage = (name, val) => window.localStorage.setItem(name, JSON.stringify(val))
export const removeLocalStorage = name => window.localStorage.removeItem(name)
export const clearlocalStorage = () => window.localStorage.clear()
/**
 * 会话存储
 * @param {String} name 
 * @returns 
 * getSessionStorage('token') ----> 获取token的值比如说为："be59044c178640c6be6d7fbcff2148af"
 * setSessionStorage('token', "asdasdaasd") ----> 设置token的值为asdasdaasd
 * removeSessionStorage('token') // 移除token
 */
export const getSessionStorage = name => safeGet(() => JSON.parse(sessionStorage.getItem(name)), window.localStorage.getItem(name))
export const setSessionStorage = (name, val) => window.sessionStorage.setItem(name, JSON.stringify(val))
export const removeSessionStorage = name => window.sessionStorage.removeItem(name)
export const clearSessionStorage = () => window.sessionStorage.clear()
/**
 * 获取操作系统类型
 * @returns 
 * @举例 getOS()   获取操作系统类型
 */
export function getOS() {
  const userAgent = 'navigator' in window && 'userAgent' in navigator && navigator.userAgent.toLowerCase() || ''
  // const vendor = 'navigator' in window && 'vendor' in navigator && navigator.vendor.toLowerCase() || ''
  const appVersion = 'navigator' in window && 'appVersion' in navigator && navigator.appVersion.toLowerCase() || ''
  if (/iphone/i.test(userAgent) || /ipad/i.test(userAgent) || /ipod/i.test(userAgent)) return 'ios'
  if (/android/i.test(userAgent)) return 'android'
  if (/win/i.test(appVersion) && /phone/i.test(userAgent)) return 'windowsPhone'
  if (/mac/i.test(appVersion)) return 'MacOSX'
  if (/win/i.test(appVersion)) return 'windows'
  if (/linux/i.test(appVersion)) return 'linux'
}
/**
 * 根据身份证获取生日、虚岁、实岁、性别
 * @param {String} idCard 身份证
 * @returns 生日、虚岁、周岁、性别、身份证脱敏
 * @举例 getIdCardInfo('321281198210185179')  对身份证进行解析
 */
export function getIdCardInfo(idCard = '') {
  if(!isIdentity(idCard)) { return {} }
  const birthDay = `${idCard.slice(6, 10)}-${idCard.slice(10, 12)}-${idCard.slice(12, 14)}`
  const inventedAge = Number(dateFormater('YYYY', new Date())) - Number(idCard.slice(6, 10)) + 1
  const realAge = Math.floor((Number(dateFormater('YYYYMMDD', new Date())) - Number(idCard.slice(6, 14))) / 10000)
  const gender = idCard.slice(-2,-1) % 2
  const genderText = gender == 1 ? '男' : '女'
  return { birthDay, inventedAge, realAge, gender, genderText, secretIdCard: idCard.slice(0, 6) + '********' + idCard.slice(-4) }
}
/**
 * 宽松相等判断
 * @param {*} a 
 * @param {*} b 
 * @returns 
 * @举例 looseEqual({name: "zz"}, {name: "dd"}) // false
 * @举例 looseEqual({}, {}) // true
 * @举例 looseEqual([{}], [{}]) // true
 */
export function looseEqual(a, b) {
  if (a === b) { return true }
  const [isReferenceA, isReferenceB, isArrayA, isArrayB] = [isReference(a), isReference(b), isArray(a), isArray(b)]
  if (isReferenceA && isReferenceB) {
    try {
      if (isArrayA && isArrayB) {
        return a.length === b.length && a.every((e, i) => looseEqual(e, b[i]))
      } else if (isDate(a) && isDate(b)) {
        return a.getTime() === b.getTime()
      } else if (!isArrayA && !isArrayB) {
        const [keysA, keysB] = [Object.keys(a), Object.keys(b)]
        return keysA.length === keysB.length && keysA.every(key => looseEqual(a[key], b[key]))
      } else {
        return false
      }
    } catch (e) {
      return false
    }
  } else if (!isReferenceA && !isReferenceB) {
    return String(a) === String(b)
  } else {
    return false
  }
}
/**
 * 生产环境消除console
 * @举例 reWriteLog()   在main.js中执行此函数
 */
export function reWriteLog(){
  console.log = (function(log){
    return process.env.VUE_APP_ENV == 'development' ? log : function() {}
  })(console.log)
}
/**
 * 获取当前是被哪个函数调用
 * @returns 
 * @举例 console.log(_.where())
 */
export function where() {
  let reg = /\s+at\s(\S+)\s\(/g
  let str = new Error().stack.toString()
  let res = reg.exec(str) && reg.exec(str)
  return res && res[1]
}
/**
 * 监听键盘事件（在需要监听的页面的created生命周期中使用）
 * @param {*} ctx 上下文
 * @param {*} obj 事件注册对象
 * @举例 
 * created(){
 *   _.listenKey(this, {
 *      ArrowLeft: () => { console.log(this.scale) }, // 左箭头的点击事件
 *      ArrowRight: () => { console.log(this.scale) }, // 右箭头的点击事件
 *   })
 * }
 */
export function listenKey(ctx = window, obj = {}) {
  const mapFn = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter', 'Control', 'Alt', 'Shift', 'Tab', 'Space', 'Delete', 'Backspace'].reduce((prev, item) => (prev[item] = obj[item] || function(){}, prev), {})
  document.onkeydown = e => {
    e = e || window.event
    // console.log(e) // 此处可以打印查看按键key值
    const key = e.key.replace(/\s+/g, '') || e.code
    mapFn[key] && mapFn[key].call(ctx)
  }
}
/**
 * 绑定事件
 * @param {String} key 
 * @param {Function} fn 
 * @举例子 addEvent('contextmenu',handleRightMouseMenu) // 绑定右键事件
 * @举例子 addEvent('keydown', handleRightMouseMenu) // 绑定键盘按下事件事件
 * --------> 处理事件
 * const handleKeyDown = e => {
 *   const key = e.key.replace(/\s+/g, '') || e.code // key ---->'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter', 'Control', 'Alt', 'Shift', 'Tab', 'Space', 'Delete', 'Backspace'
 *   if(key == 'Space') {
 *     ...
 *   }
 * }
 */
export const addEvent = (key, fn) => window.addEventListener(key,fn)
/**
 * 解绑事件
 * @param {String} key 
 * @param {Function} fn 
 * @举例子 removeEvent('contextmenu', handleRightMouseMenu) // 解绑右键事件
 * @举例子 removeEvent('keydown', handleKeyDown) // 解绑键盘事件
 */
export const removeEvent = (key, fn) => window.removeEventListener(key,fn)
/**
 * 注销键盘事件
 * @returns 
 * @举例 cancelListenKey() // 这样就取消了键盘的监听事件
 * beforeDestory(){
 *   _.cancelListenKey()
 * }
 */
export const cancelListenKey = () => document.onkeydown = e => (e || window.event).preventDefault()
/*
**********************************************************************************************
******************************************网络安全*********************************************
**********************************************************************************************
*/
// https://juejin.cn/post/7033697067390205966
// https://juejin.cn/post/7013335205054251044
// npm install xss
// import filterXSS from "xss"
/*
**********************************************************************************************
******************************************函数处理（组合拆分）*********************************
**********************************************************************************************
*/
/**
 * 组合函数
 * @param  {...Function} funcs
 * @举例 传入多个函数参数
 * @举例 const newFunc = compose(trim, upperCase) // 先去除空格，在转大写（从左往右执行）。
 * @举例 newFunc(' asfd d ')
 * @returns 组合后的函数 
 */
export const compose = (fns) => params => fns.reduce((prev, fn) => fn(prev), params) // 如果想反向可以reduceRight,反向遍历
/**
 * https://juejin.cn/post/6844903490771222542#heading-1
 * 柯里化函数
 * @param  {...Function} funcs 传入一个需要柯里化的函数
 * @举例1 var fn = curry(function(a, b, c) { console.log([a, b, c]) })
 *       var fn1 = fn('a') // 进行柯里化，传入的一个参数作为固定参数，返回新函数
 *       fn1('2', '3') // ['a', '2', '3']
 *       fn1('b', 'c') // ['a', 'b', 'c']
 * @举例2 var fn = curry(function(a, b, c) { console.log([a, b, c]);}, ['a']) // 也就是构建柯里化的时候也能顺便传入参数
 *        fn1('2', '3') // ['a', '2', '3']
 *        fn1('b', 'c') // ['a', 'b', 'c']
 * @returns 返回如果参数全了就返回计算后结果，否则产生柯里化之后的函数
 */
export function curry(fn, args = []) {
  return function() {
    var newArgs = [...args, ...arguments]
    return newArgs.length < fn.length ? curry.call(this, fn, newArgs) : fn.apply(this, newArgs)
  }
}
/**
 * 职责链模式（流程控制、跳转、递归、兜底异常捕获。用户可以指定下一步指向啥函数，默认按顺序执行）
 * 设计哲学：符合任务流的业务逻辑使用此代码，才能完美适配。否则就徒增烦恼。这就像，杀鸡你用牛刀就不合适。
 * 1、函数末尾返回next则继续执行
 * 2、函数执行有优先级，按照数组顺序
 * 3、重新调用，则从头开始匹配
 * @param {Array[...fns]} steps 职责链函数 
 * @param {Object} ctx 外部传入参数，作为上下文。可以默认不传任何参数 
 * @param {Object} 参数选项 
 * 
async function step1(ctx){
  ctx.token = 'abc123'
  console.log('Step1: 拿到token')
}
async function step2(ctx) {
  ctx.user = { id: 1, name: 'Tom' }
  console.log('Step2: 获取用户信息')
  return 'step3'
}
async function step3(ctx){
  console.log(ctx)
  console.log('Step3: 模拟失败')
  return false
}
async function step4(ctx){
  console.log('Step4: 不会执行到这里')
}
flow([step1,step2,step3,step4])
*/
export async function flow(steps = [], ctx = {}, { beforeEach = () => {}, afterEach = () => {}, onError = (err, ctx, i) => console.log(err, ctx, i) } = {}) {
  const nameToIndex = new Map(steps.map((fn, idx) => [fn.name, idx]))
  for (let i = 0; i < steps.length;) {
    const step = steps[i]
    try {
      await beforeEach(ctx, i)
      const res = await step(ctx)
      await afterEach(ctx, i, res)
      if (res === false) { break } // 返回false则不执行后续
      i = isNumber(res) ? res :
          isString(res) ? (nameToIndex.get(res) || i) : i + 1
    } catch (err) {
      await onError(err, ctx, i)
      break
    }
  }
}
/**
 * 命令模式（任务队列）
 * @params fns 需要执行的函数列表 []fn | fn
 * 1、适合批量的往调用栈里面塞函数去执行
 * 2、全量执行，不可阻断和跳过
 * 3、执行过程中可以继续往后塞入新的函数执行
export const jump = async ctx => console.log('弹跳')
export const running = async ctx => console.log('奔跑')
export const boxing = async ctx => console.log('出拳')
export const leg = async ctx => console.log('出腿')
command([running]) 
command([boxing, boxing, leg]) 
command([jump, boxing]) 
*/
export const command = (function () {
  let commandList = [] // 命令函数列表
  let isExec = false // 是否有异步任务正在执行，防止重复执行
  return async function (fns = [], ctx = {}) {
    // 如果 fns 是数组，则拼接到 commandList 中
    commandList = [...commandList, ...(isArray(fns) ? fns : [fns])]
    if (isExec) return // 如果有任务在执行，则等待新的任务加入队列
    // 启动执行队列
    isExec = true
    while (commandList.length > 0) {
      const commandFn = commandList.shift() // 获取并移除队列中的第一个命令
      await commandFn.call(this, ctx) // 执行命令，传入上下文
    }
    isExec = false // 执行完毕
  }
})()
/**
 * 函数重载生成器
 * 描述：适用重载能有效地把本来在函数内部的内省判断，分割开来。单独去实现。同时函数名能保持一致性。
 * @returns 
 * @举例子 
 * const getUser = createOverLoad()
 * getUser.addFunc('Number,Number', (page, pageSize) => {console.log('根据页码和分页参数去查找')})
 * getUser.addFunc('Number', (page) => {console.log('根据页码和分页参数去查找')})
 * getUser.addFunc('String', (name) => {console.log('根据姓名去查找')})
 * getUser.addFunc('String,String', (name, gender) => {console.log('根据姓名和性别去查找')})
 * getUser(1,2)-------> '根据页码和分页参数去查找'
 * getUser('张三', '1')-----> '根据姓名和性别去查找'
 */
export function createOverLoad() {
  const callMap = new Map()
  function overLoad(...args){
    const fn = callMap.get(args.map(v => Object.prototype.toString.call(v).slice(8, -1)).join(','))
    assert(isFunction(fn), '未找到对应的方法')
    return fn.apply(this, args)
  }
  overLoad.addFunc = (...args) => {
    const fn = args.pop()
    isFunction(fn) ? callMap.set(args.join(','), fn) : ''
  }
  return overLoad
}
/*
**********************************************************************************************
******************************************数据结构*********************************************
**********************************************************************************************
*/
/**
 * Map
 * @属性方法 .size()   .set(key, value)   .get(key)   .has(key)   .delete(key)   .clear()
 * @遍历方法 .keys()   .values()   .entries()   .forEach((val, key, map) => {console.log(val, key, map)})
 * @举例 const map = new Map([[1, 'one'], [2, 'two']]) 转数组后可以使用数组方法
 * [...map.keys()] ===> [1, 2]
 * [...map.values()] ===> ['one', 'two']
 * [...map.entires()] ===> [[1, 'one'], [2, 'two']]
 * [...map] ===> [[1, 'one'], [2, 'two']]
 */
/**
 * 键值数组转对象
 * @param {Map} Map对象
 * @举例 map2Obj(new Map([[1, 'one'], [2, 'two']]))  // {1: 'one', 2: 'two'}
 */
export function map2Obj(map){
  let obj = Object.create(null)
  for(let [k, v] of map) { obj[k] = v }
  return obj
}
/**
 * JSON转键值数组
 * @param {Map} Map对象
 * @举例 obj2Map({1: 'one', 2: 'two'}) // [[1, 'one'], [2, 'two']]
 */
export function obj2Map(obj){
  let map = new Map()
  for(let k of Object.keys(obj)) { map.set(k, obj[k]) }
  return map
}
/**
 * 数组转对象
 * @param {Array} arr 需要转为对象的数组
 * @returns 
 * @举例 arr2Obj([{a:1, b:4, c:7}, {a:2, b:5, c: 8}, {a:3, b:6, c: 9}]) ----> {a: [1,2,3], b: [4, 5, 6], c: [7,8,9]}
 * @举例 arr2Obj(['a', 'b']) ----> {0:'a', 1:'b'}
 */
export function arr2Obj(arr){
  return arr.reduce((prev, item, index) => (isObject(item) ? Object.entries(item).forEach(p => (prev[p[0]] = [...(prev[p[1]] || []), p[1]])) : (prev[index] = item), prev), {})
}
/**
 * 将一个属性值为数组的一个集合，处理成对象数组
 * @param {*} obj 
 * @举例子 
 *  const obj = {a: [1,2,3], b: [4, 5, 6], c: [7,8,9]}
 *  obj2Arr(obj)
 * ------->  [{a:1, b:4, c:7}, {a:2, b:5, c: 8}, {a:3, b:6, c: 9}]
 */
export function obj2Arr(obj){
  const maxLen = Math.max(...Object.values(obj).map(v => v.length))
  return new Array(maxLen).fill('').reduce((prev, item, index) => [...prev, Object.entries(obj).reduce((p, v) => (p[v[0]] = v[1][index], p), {})], [])
}
/**
 * 字符串转数组
 * @param {String} v 
 * @param {*} split 
 * @returns 
 * str2Arr('a,b,c') ----> ['a','b','c']
 */
export function str2Arr(v, split = ',') {
  return isString(v) ? v.split(split) : v
} 
/**
 * 数组转字符串
 * @param {*} v 
 * @param {*} split 
 * @returns 
 * arr2Str(['a','b','c']) ----> 'a,b,c'
 */
export const arr2Str = (v, split = ',') => isString(v) ? v : v.join(split)
/**
 * 二维数组行列互换
 * @param {Array<Array>} arr 需要行列互换的二维数组
 * @returns {Array<Array>} 
 * @举例  transformArr([['a','b','c'], [0, 1, 2]]) ---> [['a', 0], ['b', 1], ['c', 2]]
 */
export const transformArr = arr => arr[0].map((col, i) => arr.map(row => row[i]))
/**
 * 逆转对象。（键值互换）
 * @param {Object} obj 需要逆转的对象
 * @param {String} type 是否丢弃相同key的字段。默认不丢弃   'drop'为丢弃重复键值
 * @举例子 invertObj({ 'a': 1, 'b': 2, 'c': 1 }) -----> {1: ['a', 'c'], 2: ['b']}
 * @举例子 invertObj({ 'a': 1, 'b': 2, 'c': 1 }, 'drop') -----> {1: 'c', 2: 'b'}
 * @returns 
 */
export function invertObj(obj = {}, type = 'keep') {
  return Object.keys(obj).reduce((prev, item) => {
    if(type == 'keep') {
      (prev[obj[item]] || (prev[obj[item]] = [])).push(item)
    } else {
      prev[obj[item]] = item
    }
    return prev
  }, {})
}
/**
 * 逆转Map
 * @param {Array[Array]} twoDArr 二维数组
 * @param {String} type 是否丢弃相同key的字段。默认不丢弃   'drop'为丢弃重复键值
 * @returns 
 */
export function invertMap(twoDArr = [], type = 'keep'){
  return [...twoDArr].reduce((prev, [key, val]) => {
    if(type == 'keep') {
      prev.has(val) ? prev.get(val).push(key) : prev.set(val, [key])
    } else {
      prev.set(val, key)
    }
    return prev
  }, new Map())
}
/**
 * 使用 PerformanceObserver 监听 fcp。计算白屏时间
 */
export function getFcpTime(){
  if (!!PerformanceObserver){
    try {
      const type = 'paint'
      if ((PerformanceObserver.supportedEntryTypes || []).includes(type)) {
        const observer = new PerformanceObserver((entryList)=>{
          for(const entry of entryList.getEntriesByName('first-contentful-paint')){
            const { startTime } = entry
            console.log(`首屏加载时间：${startTime}ms`)
          }
        })
        observer.observe({ entryTypes: [type] })
      }
    } catch (e) {
      console.warn('[assets-load-monitor] PerformanceObserver error:', (e || {}).message ? e.message : e)
    }
  }
}
/**
 * 任务调度器。当我们需要做一个长耗时的任务的时候，可以使用js调度器去让cpu有执行间隙去渲染dom
 * @param {*} tasks 任务数组，
 * @param {*} scheduler 调度器，决定多长时间执行一次，以及一次执行多久
 * 自定义传入scheduler
 * ------------>
  const sheduler = chunkTask => {
    setTimeout(() => { // 定义每隔多长时间开始执行任务
      const now = performance.now()
      chunkTask(() => performance.now() - now < 1) // 定义每次执行1ms
    }, 1e3) // 间隔1s
    chunkTask()
  }
 * @returns 
 * @举例 往网页中插入30000个dom
  performTask(Array.from({length: 30000}, (_, i) => () => {
    const div = document.createElement('div')
    div.innerText = i
    document.body.appendChild(div)
  }))
 */
export function performTask(tasks, scheduler = chunkTask => requestIdleCallback(deadLine => chunkTask(() => deadLine.timeRemaining() > 0))){
  if(!tasks.length) { return }
  let i = 0
  const _run = () => scheduler((goOn) => {while(i < tasks.length && goOn()) { tasks[i++]() };_run()})
  _run()
}
// 优化参数必填
function need(v){
  throw new Error(`${v || ''}参数必填`)
}
/**
 * 初始化canvas
 * @param {String} val dom 或者  id 值
 * @returns 
 * initCanvas('id') // _.initCanvas(_.getDom('id'))
 */
export function initCanvas(val = need('canvas-dom'), width = need('canvas高度'), height = need('canvas宽度')) {
  const canvasRef = isDom(val) ? val : getDom(val)
  if(!canvasRef) { return {} }
  const ctx = canvasRef.getContext('2d')
  // const dpr = window.devicePixelRatio || 1
  const dpr = 1
  // const { width, height } = canvasRef
  // console.log(width, height)
  // 重新设置 canvas 自身宽高大小和 css 大小。放大 canvas；css 保持不变，因为我们需要那么多的点
  canvasRef.width = Math.round(width * dpr)
  canvasRef.height = Math.round(height * dpr)
  canvasRef.style.width = width + 'px'
  canvasRef.style.height = height + 'px'
  // 直接用 scale 放大整个坐标系，相对来说就是放大了每个绘制操作
  ctx.scale(dpr, dpr)
  return { canvasRef, ctx, width, height }
}